/* Type in this file is expressed in rem (see the Sept 2026 HIG pass): the
   scale follows the reader's own text-size setting instead of ignoring it.
   Divide by 16 to read a value back in pixels — 0.8125rem is 13px. Lengths
   that are not type stay in px on purpose. */
/* The hidden attribute always hides. Any class that sets its own display
   (flex, block, inline-block) otherwise beats the browser's built-in
   [hidden]{display:none}, so JS setting el.hidden silently did nothing.
   Found leaking in four places on Sept 14 2026: the Profile page's Remove
   photo and Resend verification buttons (after .secondary-btn gained an
   explicit display), the Avatar background colour row, and the Elements
   filter row. The per-class [hidden] rules further down predate this and
   are now redundant but harmless. */
[hidden] { display: none !important; }

/* ===========================================================
   AI Studio — a self-hosted generation console.
   Silver / black / white theme — brushed-metal surfaces, black
   borders and lettering, white where readability needs it. The
   always-dark generation-output ("viewfinder") surfaces are the
   one deliberate exception — see the --vf-* tokens below — since
   those read as a camera display, not page chrome.
   =========================================================== */

:root {
  /* Exact silver / black / white palette. */
  --silver-bright: #F7F8F8;
  --silver-light: #D8DDE1;
  --silver-mid: #B0B6BD;
  --silver-polished: #EEF1F3;
  --silver-dark-reflective: #9AA1A8;
  --silver-secondary: #D8DDE0;
  --silver-bright-return: #F6F7F8;
  --pure-black: #0A0A0A;
  --text-primary-black: #111111;
  --text-secondary-dark: #3E4348;
  --white-text: #F8F8F8;
  --black-dots: rgba(0, 0, 0, 0.38);

  /* The exact metallic silver gradient — used on the main silver surfaces
     (sidebar, panels/cards). Rebalanced toward the mid/darker stops from
     the same approved palette so the surface reads as true brushed metal
     rather than a near-white wash. */
  --surface-gradient: linear-gradient(
    135deg,
    #EEF1F3 0%,
    #D8DDE1 16%,
    #B0B6BD 32%,
    #9AA1A8 48%,
    #B0B6BD 64%,
    #D8DDE1 80%,
    #EEF1F3 100%
  );

  --bg: var(--silver-mid);
  --surface: var(--silver-light);
  --surface-2: var(--silver-polished);
  --surface-3: var(--silver-dark-reflective);
  --border: var(--pure-black);
  --border-soft: rgba(10, 10, 10, 0.35);

  --text: var(--text-primary-black);
  --text-muted: var(--text-secondary-dark);
  --text-faint: rgba(62, 67, 72, 0.86); /* was .72: 4.16:1 on silver, below AA. .86 clears 4.5 on every chassis surface */

  /* Color-hierarchy tokens (visual-hierarchy pass): these exist so a field/
     box heading ("PROMPT", "ASPECT RATIO", "MODEL"…) reads as clearly
     brighter than the helper text under it, without touching --text (page
     titles, active states) or --text-muted/--text-faint (helper copy,
     hints, counts, secondary metadata), which are unchanged and still used
     exactly as before everywhere they already appear. */
  --text-label: var(--text-primary-black);

  /* Sidebar-navigation-specific tokens — kept separate from --text-muted/
     --text-faint (used broadly elsewhere for helper text/hints) so
     brightening inactive nav items doesn't brighten unrelated helper text
     that happens to share those tokens. */
  --nav-text-inactive: var(--text-secondary-dark);
  --nav-icon-inactive: var(--text-secondary-dark);
  --nav-group-label: rgba(17, 17, 17, 0.68); /* was .55 (4.02:1); .68 clears AA on silver-polished */

  --accent: var(--pure-black);
  --accent-soft: rgba(10, 10, 10, 0.08);
  --accent-soft-2: rgba(10, 10, 10, 0.16);

  --teal: #2fd6c8;
  --teal-soft: rgba(47, 214, 200, 0.14);

  --gold: #f0b429;
  --gold-soft: rgba(240, 180, 41, 0.14);

  --success: #2f8f52;
  --success-soft: rgba(47, 143, 82, 0.14);
  --warning: #a5720a;
  --warning-soft: rgba(165, 114, 10, 0.14);
  --error: #b23a3a;
  --error-soft: rgba(178, 58, 58, 0.14);
  --danger: #a51e23;
  --danger-soft: rgba(165, 30, 35, 0.16);

  --font-display: 'Archivo', 'Arial Narrow', sans-serif;
  /* App-wide sans-serif system. --font-mono is kept only so the many
     existing `font-family: var(--font-mono)` declarations across this file
     (technical/uppercase labels, credit numbers, history rows, node meta,
     etc.) don't need to be hand-edited one by one — it now resolves to the
     exact same sans-serif stack as everything else, so nothing in the app
     renders in a monospace/typewriter face anymore. Their existing weight/
     uppercase/letter-spacing styling (the actual source of their "technical"
     look) is untouched. Do not point this back at a monospace stack. */
  --font-ui: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-body: var(--font-ui);
  --font-mono: var(--font-ui);

  --radius: 8px;
  --radius-lg: 14px;

  /* Elevation + metallic sheen — the depth/shine layer that makes panels,
     cards and buttons read as a brushed-metal instrument rather than flat
     boxes. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.22);
  --shadow-md: 0 10px 28px -10px rgba(0, 0, 0, 0.32), 0 2px 8px rgba(0, 0, 0, 0.16);
  --shadow-lg: 0 20px 48px -12px rgba(0, 0, 0, 0.38), 0 4px 14px rgba(0, 0, 0, 0.22);
  --shadow-glow: 0 0 0 1px rgba(10, 10, 10, 0.3), 0 12px 36px -8px rgba(10, 10, 10, 0.18);
  --inset-hairline: inset 0 1px 0 rgba(247, 248, 248, 0.8);
  --panel-sheen: linear-gradient(160deg, rgba(247, 248, 248, 0.5), rgba(247, 248, 248, 0) 45%);
  --accent-gradient: var(--accent);

  /* Dedicated tokens for the always-dark generation-output ("viewfinder")
     surfaces — a camera-style black display for generated images/video,
     kept dark on purpose regardless of the silver theme above. These are
     an exact mirror of the app's original dark-theme values. */
  --vf-bg: #0a0a0c;
  --vf-surface: #141417;
  --vf-surface-2: #1a1a1f;
  --vf-surface-3: #212127;
  --vf-border: #28282f;
  --vf-border-soft: #1c1c22;
  --vf-text: #f2f2f0;
  --vf-text-muted: #93939c;
  --vf-text-faint: #7a7a82;
  --vf-text-label: #dcdce0;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  max-width: 100%;
}

body {
  margin: 0;
  font-family: var(--font-ui);
  background: var(--surface-gradient);
  background-attachment: fixed;
  color: var(--text);
  font-size: 0.875rem;
  line-height: 1.5;
}

/* Mobile-only horizontal-scroll backstop: html/body/#app-shell are capped to
   the viewport's own width and any accidental horizontal overflow is clipped
   here rather than becoming page-level sideways movement. This is a safety
   net on top of (not instead of) fixing the actual overwide sources found
   under the max-width:860px rules elsewhere in this file — see those rules
   for the real fixes. Scoped to mobile only: desktop's independently-
   scrolling `main` (see the min-width:861px block below) already sets its
   own overflow and must not be touched. touch-action:pan-y is applied only
   to body, the actual vertical document-scroll surface on mobile (main has
   no scroll of its own at this width) — never to the Workflow canvas or any
   local horizontal scroller, which keep their own touch-action/overflow. */
@media (max-width: 860px) {
  html, body {
    overflow-x: hidden;
    width: 100%;
    touch-action: pan-y;
  }
  #app-shell {
    max-width: 100vw;
    overflow-x: hidden;
  }
}

/* Native form controls don't inherit font-family by default in most
   browsers — force them onto the same sans-serif system so no <button>,
   <input>, <select>, <textarea>, or <option> can end up on the browser's
   own default (frequently a monospace/serif UA stylesheet font) instead of
   this app's typography. Purely a font-family/font fix — sizes, weights,
   colors, and dimensions for these elements all come from their existing
   component-level rules elsewhere in this file, untouched. */
button, input, select, textarea, option {
  font: inherit;
  font-family: var(--font-ui);
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

/* ---------- layout shell ---------- */

.app {
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  min-height: 100vh;
}

/* Landing/intro state: while the Home view is showing (before ENTER is
   pressed), the sidebar is hidden completely and the landing content uses
   the full viewport width — no reserved sidebar column. This is a pure
   visibility/state toggle (see #app-shell.landing-active toggled in
   showView() in app.js) — the .rail markup, styling and width are
   untouched, and everything reverts the instant a non-home view loads. */
#app-shell.landing-active {
  grid-template-columns: minmax(0, 1fr);
}
#app-shell.landing-active .rail {
  display: none;
}

.rail-toggle {
  display: none;
  margin-left: auto;
  /* Same brushed-metal treatment as .rail-collapse-btn so the mobile
     hamburger control blends with the silver rail instead of sitting on
     it as a flat pale swatch. */
  background: linear-gradient(160deg, #f4f5f6 0%, #d6d8db 45%, #b7bac0 100%);
  border: 1px solid rgba(10, 10, 10, 0.3);
  color: var(--text);
  width: 34px; height: 34px; border-radius: var(--radius);
  font-size: 1rem; cursor: pointer;
  align-items: center; justify-content: center;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.5);
}

.rail-panel {
  display: flex;
  flex-direction: column;
  gap: 28px;
  flex: 1;
  min-height: 0;
}

@media (max-width: 860px) {
  .app { grid-template-columns: minmax(0, 1fr); display: block; }
  .rail {
    /* !important on z-index too, not just position: the later, unconditional
       ".rail { position: relative; z-index: 1; }" rule further down this file
       (desktop layout) has equal selector specificity, so without !important
       here it wins the cascade tie by source order and silently drops this
       mobile menu's stacking level to 1 — same as the Studio tiles and every
       other bit of page content, which then win ties against it by simply
       coming later in the DOM. That's what let page content render on top of
       the open mobile menu. Bumped well above every other z-index in this
       stylesheet (highest elsewhere is 1000, for desktop-only modals that
       never show at this width) so the open mobile menu is unambiguously the
       top layer, full stop. */
    position: fixed !important; top: 0; left: 0; right: 0; z-index: 9999 !important;
    /* Closed, this bar holds only the brand row — keep its padding tight so
       its real height matches what `main`'s padding-top below compensates
       for. The old unconditional bottom padding here (32px + safe-area) was
       sized for the *open*, scrollable menu (see .rail.open below) but was
       being applied even when closed, inflating the collapsed bar well past
       70px and pushing it over the top of every page's content — including
       the "← Back" buttons, which is why they were going missing on mobile. */
    padding: max(8px, env(safe-area-inset-top)) 16px 8px 16px !important;
    max-height: 100vh;
    overflow-y: auto;
    box-shadow: 0 2px 14px rgba(0, 0, 0, 0.35);
  }
  /* Bottom padding needs real headroom, not just the safe-area inset, but
     only once the menu is open and scrollable: iOS Safari's floating
     URL/toolbar sits *over* the bottom of the viewport (it isn't excluded
     from 100vh the way a docked bar would be), so without this the account
     card at the end of the scrollable panel would render right underneath
     that overlay. */
  .rail.open {
    padding-bottom: calc(32px + env(safe-area-inset-bottom)) !important;
  }
  /* !important on all four of these: the unconditional (desktop) versions
     of these same rules sit LATER in this file (.rail-brand, .rail-brand-
     controls, .rail-brand-logo below), so without !important they'd win
     the cascade tie by source order despite this media query's condition
     actually matching — same reasoning as the .rail rule's !important
     above. */
  /* Header collapsed from three stacked pieces (controls row / logo / BETA
     badge) down to ONE row: logo and badge sit side by side, vertically
     centered, and the hamburger is pulled out of flow into the corner (also
     vertically centered) instead of holding its own row above everything.
     With nothing stacked, the header's height is just "logo + a little
     padding" — which is what lets the logo itself grow larger while the
     bar gets shorter, instead of those two goals fighting each other. */
  .rail-brand {
    flex-direction: row !important;
    align-items: center !important;
    justify-content: center !important;
    /* Trimmed from 10px top/bottom to 4px — this is the "headroom" the
       enlarged logo below spends instead, without the row (and therefore
       the bar) getting any taller than it was. */
    padding: 4px 50px 4px 14px !important;
    gap: 12px !important;
    position: relative;
  }
  .rail-collapse-btn { display: none !important; }
  .rail-toggle {
    display: flex !important;
    position: absolute !important;
    top: 50%; right: 14px;
    transform: translateY(-50%);
    width: 28px; height: 28px;
    font-size: 0.875rem;
    border-color: var(--accent);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
  }
  /* No longer its own row (rail-toggle is absolutely positioned above), so
     this just needs to not reserve any row height or width of its own. */
  .rail-brand-controls { display: contents; }
  /* Sized by max-height rather than max-width: the row's height is what's
     capped (padding above was cut to free up exactly this much room), so
     driving size from height guarantees the logo fills that space as large
     as it can go — width follows automatically from the source image's own
     aspect ratio (object-fit: contain), so the artwork is never stretched,
     distorted, or cropped. This is the largest the logo can render without
     the row — and therefore the silver bar itself — growing any taller. */
  .rail-brand-logo {
    max-width: none !important;
    max-height: 92px !important;
    width: auto !important;
    height: 92px;
    margin: 0 !important;
    flex-shrink: 0;
  }
  /* Beside the logo now, not underneath it — this is what let the logo grow
     without the header growing too. Nudged further from the logo now that
     the logo itself is wider, and shifted right along with it since the
     group as a whole stays centered. */
  /* PHASE 7 CORRECTION: this row (logo + badge, both flex-shrink:0 by
     design) can be wider than the space actually left after the toggle
     button's reserved 50px of right padding — confirmed live at 375px
     wide: the badge's right edge (x=329) overlapped the hamburger
     toggle's left edge (x=316), clipping the "A" in "BETA" behind the
     button. Letting the badge (the less essential of the two — the
     wordmark/logo must stay full size) shrink and truncate instead of
     colliding with the toggle fixes this without touching the logo's
     sizing rules above. */
  .rail-beta-badge {
    flex-shrink: 1;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .rail-brand { overflow: hidden; }
  .rail-panel {
    display: none;
    flex-direction: column;
    gap: 28px;
    margin-top: 18px;
  }
  .rail.open .rail-panel { display: flex; }
  .rail-account { margin-top: 0; padding-top: 16px; }
  /* Kept in sync with the closed header's real rendered height (padding +
     the single logo/badge row, now that nothing stacks) so page content
     never starts underneath the fixed silver bar. */
  main {
    padding-top: calc(100px + env(safe-area-inset-top)) !important;
  }
  main:has(> #view-home.active) {
    padding-top: 0 !important;
  }
  /* Landing/intro state on mobile: hide the fixed top bar (hamburger +
     SHOTBORN emblem) entirely while on the Home view, with no reserved
     header space — main's padding-top is already zeroed for #view-home
     above. Reverts the instant a non-home view loads. */
  #app-shell.landing-active .rail {
    display: none !important;
  }
}

.rail {
  background:
    radial-gradient(120% 40% at 18% -4%, rgba(0, 0, 0, 0.09), transparent 62%),
    var(--surface-gradient);
  border-right: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  padding: 22px 16px;
  display: flex;
  flex-direction: column;
  gap: 28px;
  position: relative;
  z-index: 1;
  transition: padding 0.18s ease;
}

.rail-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 10px 10px 16px;
}

/* Compact row holding just the collapse/hamburger controls, right-aligned
   above the centered logo — kept in normal flow (not absolutely positioned)
   so it can never overlap the logo image below it. A bit of extra margin
   below gives the logo breathing room instead of sitting jammed under the
   control row (the parent's own gap alone was too tight). */
.rail-brand-controls {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  width: 100%;
  gap: 8px;
  margin-bottom: 6px;
}

/* Full "AI Image & Video Studio" logo image is the normal sidebar brand;
   the small circular mark only appears once the rail collapses down to
   icon-only width. Sized to sit centered with clean, even side margins —
   width/max-width bound it to a sidebar-safe size, height:auto + contain
   preserve the source aspect ratio exactly so nothing stretches, distorts,
   or gets cropped. */
.rail-brand-logo {
  display: block;
  width: 100%;
  max-width: 210px;
  height: auto;
  object-fit: contain;
  margin: 0 auto;
}
/* Collapsed rail shows no logo/icon at all — just the collapse toggle. */
#app-shell.rail-collapsed .rail-brand-logo { display: none; }

/* Small "BETA" tag next to the wordmark, signaling the product is still
   under active development. Hidden once the rail collapses to icon-only
   width — there's no room for it there and the mark alone is enough. */
.rail-beta-badge {
  flex-shrink: 0;
  font-family: var(--font-body);
  font-size: 0.59375rem;
  font-weight: 700;
  letter-spacing: 0.5px;
  color: var(--accent);
  background: var(--accent-soft);
  border: 1px solid var(--accent-soft-2);
  border-radius: 999px;
  padding: 2px 7px;
  line-height: 1.4;
}
#app-shell.rail-collapsed .rail-beta-badge { display: none; }

.rail-collapse-btn {
  flex-shrink: 0;
  /* Brushed-metal chip instead of a flat pale swatch — a soft silver-to-
     graphite gradient plus a hairline border keyed to the rail's own
     surface gradient, so the control reads as part of the metallic
     sidebar rather than a plain white pill sitting on top of it. */
  background: linear-gradient(160deg, #f4f5f6 0%, #d6d8db 45%, #b7bac0 100%);
  border: 1px solid rgba(10, 10, 10, 0.3);
  color: var(--text-muted);
  width: 26px; height: 26px; border-radius: var(--radius);
  font-size: 0.75rem; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.5);
  transition: border-color 0.18s ease, color 0.18s ease, transform 0.18s ease, background 0.18s ease;
}
@media (hover: hover) and (pointer: fine) {
  .rail-collapse-btn:hover { border-color: var(--accent); color: var(--text); }
}

.rail-brand-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.9375rem;
  letter-spacing: 0.2px;
  line-height: 1.2;
}

.rail-brand-tag {
  font-family: var(--font-mono);
  font-size: 0.59375rem;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.4px;
  margin-top: 1px;
}

.rail-divider {
  height: 1px;
  background: var(--border-soft);
  margin: 8px 6px;
}

.rail-nav {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.rail-group-label {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--nav-group-label);
  padding: 14px 10px 6px;
  opacity: 0.85;
}
.rail-group-label:first-child { padding-top: 2px; }

.rail-btn {
  display: flex;
  align-items: center;
  gap: 11px;
  background: transparent;
  border: none;
  border-left: 2px solid transparent;
  color: var(--nav-text-inactive);
  padding: 10px 12px 10px 10px;
  border-radius: var(--radius);
  font-family: var(--font-body);
  font-size: 0.875rem;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
  position: relative;
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}

.rail-btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--nav-icon-inactive);
  font-size: 0.875rem;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  text-align: center;
  transition: color 0.18s ease;
}

.rail-btn-label { transition: color 0.18s ease; }

/* Hover is gated to devices that actually have a hovering pointer.
   
   A touch browser (iOS Safari most stubbornly) keeps :hover applied to the
   last element tapped until something else is tapped. Because this rule
   paints the same kind of filled pill as .rail-btn.active, the row the user
   last tapped stayed lit long after navigating away, and looked exactly like
   the current page. Tapping Shot Extractor and then moving on left two rows
   apparently selected at once.

   Gating it means touch devices get no hover state at all, so the ONLY filled
   row is .rail-btn.active — which app.js already recomputes from the current
   route on every navigation, including browser Back/Forward. Nothing changes
   for mouse users.

   These stay ABOVE .rail-btn.active on purpose: .rail-btn:hover and
   .rail-btn.active have identical specificity (0,2,0), so source order is
   what makes the active pill win when you hover the current page's own row.
   Moving this block below .active would silently invert that. */
@media (hover: hover) and (pointer: fine) {
  .rail-btn:hover {
    background: linear-gradient(90deg, rgba(0, 0, 0, 0.06), var(--surface-2) 70%);
    color: var(--text);
  }
  .rail-btn:hover .rail-btn-icon { color: var(--text-muted); }
}

.rail-btn.active {
  background: linear-gradient(90deg, rgba(0, 0, 0, 0.18), var(--surface-3) 75%);
  border-left-color: var(--accent);
  color: var(--text);
  font-weight: 600;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 0 14px -4px rgba(0, 0, 0, 0.4);
}

.rail-btn.active .rail-btn-icon { color: var(--accent); filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.65)); }

.rail-footer {
  margin-top: auto;
  padding-top: 16px;
  border-top: 1px solid var(--border-soft);
}

.rail-footer-label {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  color: var(--nav-group-label);
  margin-bottom: 10px;
  padding: 0 6px;
}

.provider-status-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0 6px;
}

.provider-status-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.78125rem;
  color: var(--text-muted);
}

.status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-faint);
  flex-shrink: 0;
}

.status-dot.on { background: var(--success); box-shadow: 0 0 0 3px var(--success-soft); }

.rail-account {
  margin-top: auto;
  padding: 12px;
  border-top: 1px solid var(--border-soft);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  border-radius: var(--radius-lg);
  background: linear-gradient(160deg, var(--surface-2), var(--surface));
  border: 1px solid var(--border-soft);
  width: 100%;
  font-family: var(--font-body);
  text-align: left;
  cursor: pointer;
  transition: background 0.18s ease, border-color 0.18s ease;
}
/* Same sticky-hover defect as .rail-btn above — the account row also holds a
   filled background after a tap on touch. */
@media (hover: hover) and (pointer: fine) {
  .rail-account:hover {
    background: linear-gradient(160deg, var(--surface-3), var(--surface-2));
    border-color: var(--border);
  }
}
.rail-account-top { display: flex; align-items: center; gap: 10px; min-width: 0; }
.account-avatar {
  width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(145deg, var(--surface-3), var(--surface-2));
  border: 1px solid var(--border);
  box-shadow: var(--inset-hairline), 0 0 0 3px rgba(0, 0, 0, 0.14);
  color: var(--accent); font-size: 0.8125rem; font-weight: 700; font-family: var(--font-display);
  overflow: hidden;
}
.account-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.account-meta { min-width: 0; flex: 1; }
.account-name { font-size: 0.8125rem; font-weight: 700; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-sub {
  font-family: var(--font-mono); font-size: 0.65625rem; color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail-account-chevron { margin-left: auto; flex-shrink: 0; color: var(--text-faint); font-size: 0.9375rem; transition: color 0.18s ease; }
@media (hover: hover) and (pointer: fine) {
  .rail-account:hover .rail-account-chevron { color: var(--text-muted); }
}
#app-shell.rail-collapsed .rail-account-chevron { display: none; }

/* Live credit-balance row — sourced from the exact same currentAccount.
   credit_balance the Settings page renders (see updateAccountPanel() in
   app.js, which sets both in the same pass), never a separate counter.
   Hidden entirely in local/self-hosted mode where there's no credit
   concept at all (same condition Settings' own credit panel uses). */
.rail-account-credits {
  display: flex;
  align-items: center;
  gap: 6px;
  padding-top: 8px;
  border-top: 1px solid var(--border-soft);
  font-family: var(--font-mono);
  font-size: 0.71875rem;
  font-weight: 700;
  color: var(--text);
  min-width: 0;
}
.rail-account-credits-icon { color: var(--text-muted); font-size: 0.75rem; flex-shrink: 0; }
.rail-account-credits-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-account-credits-unit { color: var(--text-faint); font-weight: 600; letter-spacing: 0.4px; }
#app-shell.rail-collapsed .rail-account-credits { display: none; }
/* .rail-account-credits sets its own display:flex above, which (being an
   author-stylesheet rule) otherwise wins over the browser's default
   [hidden]{display:none} UA rule — so JS setting the `hidden` property
   (see applyProfileToSidebar()'s guest branch in app.js) silently had no
   visual effect and a guest could see a stale/fake credit row. This raises
   the attribute selector's specificity above the plain class rule so
   `hidden` actually hides it. */
.rail-account-credits[hidden] { display: none; }
#app-shell.rail-collapsed .rail-account { align-items: center; }

/* ---------- collapsed rail (desktop) ---------- */

#app-shell.rail-collapsed { grid-template-columns: 76px minmax(0, 1fr); }
#app-shell.rail-collapsed .rail { padding-left: 12px; padding-right: 12px; }
#app-shell.rail-collapsed .rail-brand { justify-content: center; gap: 0; }
#app-shell.rail-collapsed .rail-brand-name,
#app-shell.rail-collapsed .rail-brand-tag,
#app-shell.rail-collapsed .rail-group-label,
#app-shell.rail-collapsed .rail-btn-label,
#app-shell.rail-collapsed .account-meta { display: none; }
#app-shell.rail-collapsed .rail-collapse-btn { margin-left: 0; transform: rotate(180deg); }
#app-shell.rail-collapsed .rail-btn { justify-content: center; padding: 10px; gap: 0; }
#app-shell.rail-collapsed .rail-account { justify-content: center; padding: 10px 6px; }

main {
  padding: 36px 44px 60px;
  max-width: 1180px;
  width: 100%;
  min-width: 0;
}

/* ---------- fixed sidebar / independently-scrolling main (desktop) ----------
   The sidebar (.rail) must never move when the page scrolls: it stays pinned
   full-height while only the content to its right scrolls. Achieved by
   pinning the .app grid itself to the viewport height with no scroll of its
   own, then letting .rail and main each independently overflow-y:auto within
   that fixed-height row. Scoped to desktop widths only — the 860px mobile
   breakpoint above already has its own fixed-header + normal document-scroll
   layout and is left untouched. */
@media (min-width: 861px) {
  html, body { height: 100%; overflow: hidden; }
  /* 100dvh (dynamic viewport height) is the CURRENTLY-visible height; 100vh on
     iOS Safari is the larger toolbar-hidden height, so in iPad landscape (with
     the tab bar showing) a 100vh shell overshoots the visible bottom. That
     overshoot is what exposed the silver body background as a "white bar" along
     the bottom and pushed the left tool dock's top:50% center below the fold
     (the Hand/Select tools appearing to hide). dvh matches the visible box
     exactly on every side; the 100vh line stays as the fallback for the few
     engines without dvh. */
  .app { height: 100vh; height: 100dvh; min-height: 0; overflow: hidden; }
  .rail { height: 100vh; height: 100dvh; overflow-y: auto; overflow-x: hidden; }
  main {
    height: 100vh;
    height: 100dvh;
    overflow-y: auto;
    scrollbar-width: none; /* Firefox */
    -ms-overflow-style: none; /* old Edge/IE */
  }
  main::-webkit-scrollbar { display: none; width: 0; height: 0; } /* WebKit/Chromium */

  /* Studio Director docked layout (desktop only): opening the drawer pushes
     the whole app shell's right edge in by the drawer's width instead of
     the drawer sitting on top of the page. .app is already a 2-column grid
     (rail + main); shrinking the shell's own box via margin-right lets that
     grid's `minmax(0, 1fr)` main column reflow naturally — real available
     width changes, so cards/grids/breakpoints inside each page respond on
     their own. No transform:scale, no per-page changes. The left sidebar
     (.rail) is the grid's other, fixed-width column, so it never moves.
     Below this breakpoint the rule doesn't apply at all — mobile/tablet
     keeps the drawer's existing full-width overlay/bottom-sheet behavior
     (see the max-width: 860px .sd-drawer block below). */
  #app-shell {
    transition: margin-right 0.22s ease;
  }
  #app-shell.sd-open {
    /* Matches .sd-drawer's own width below exactly, so the reflowed
       workspace ends flush against the drawer's left edge. */
    margin-right: 380px;
  }
}

/* The workflow view breaks out of main's padding via negative margins
   (see #view-workflow below) so its canvas can span the full remaining
   viewport width instead of being boxed into the normal 1180px reading
   column. Negative margins alone only claw back the padding though —
   main's own max-width still caps the whole element's box, silently
   truncating the canvas well short of the real edge on wide viewports
   (a flat black dead zone with no dots, node-canvas-layer's inset:0
   notwithstanding). Lift the cap on main itself whenever the workflow
   view is the active child so the canvas can actually reach full width. */
main:has(> #view-workflow.active) {
  max-width: none;
  position: relative;
  /* The node canvas below already paints its own dots + silver, but the
     header region above it (Back/title/toolbar/Save) previously sat on
     plain silver with no dots — a visible seam. Painting the same brushed
     silver + dot treatment on `main` itself makes the whole page one
     continuous surface; #view-workflow and its children already stack
     above this via position:relative/z-index:1 (below), so nothing about
     the canvas, toolbar, or node behavior changes. */
  background:
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
}
main:has(> #view-workflow.active)::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: radial-gradient(var(--black-dots) 1px, transparent 1px);
  background-size: 22px 22px;
  pointer-events: none;
  z-index: 0;
}

/* Home view is a full-bleed looping video background — break out of
   main's padding/max-width the same way the workflow canvas does. */
main:has(> #view-home.active) {
  max-width: none;
  padding: 0;
}

.home-video-wrap {
  position: relative;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  min-height: 480px;
  overflow: hidden;
  background: #000;
}
.home-video {
  /* Full-bleed hero background: the video covers the entire wrap edge to
     edge with no inset margins — object-fit: cover scales it to fill the
     box completely (cropping a little off the outer edges as needed)
     rather than letterboxing it down to fit inside a padded box. */
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
  display: block;
}
/* Scrim removed — the video's own black is meant to blend straight into the
   page background (especially on mobile's letterboxed contain fit), so no
   grey wash sits on top of it any more. */
.home-video-scrim {
  display: none;
}
.home-enter-btn {
  position: absolute;
  bottom: 10%; left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  border-radius: 999px;
  padding: 16px 48px;
  /* Polished silver fill — matches the brushed-metal Studio icon set instead
     of the old warm-orange ember gradient. Kept as a flat metallic sheen
     (no glow/neon), with a thin graphite border standing in for the ember
     button's borderless look. */
  background: linear-gradient(160deg, #f0f0f2 0%, #c7c8cc 45%, #9a9ba0 75%, #c2c3c7 100%);
  color: #14141a;
  border: 1px solid #2a2a2e;
  font-family: var(--font-display); font-weight: 800; font-size: 1rem;
  letter-spacing: 1.5px;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.home-enter-btn:hover {
  /* Dark gunmetal/graphite fill on hover, white text, a subtle silver
     border highlight — restrained, no drop-shadow escalation. */
  background: linear-gradient(160deg, #3c3d42 0%, #2a2b2f 50%, #1b1c1f 100%);
  color: #ffffff;
  border-color: rgba(220, 222, 226, 0.55);
  transform: translateX(-50%) translateY(-2px);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
}
.home-enter-btn:active { transform: translateX(-50%) translateY(1px); }

/* Short desktop windows (laptop with browser chrome, split-screen, 1366x768
   at 125% zoom, etc.): pull the Enter button in from the edge a bit so it
   stays comfortably inside a shorter viewport rather than sitting right at
   the visual bottom edge. The video itself always covers the full box via
   object-fit: cover, so no cropping/letterboxing logic is needed here. */
@media (max-height: 700px) and (min-width: 641px) {
  .home-enter-btn {
    bottom: 6%;
    padding: 13px 40px;
    font-size: 0.875rem;
  }
}

@media (max-width: 640px) {
  .home-video-wrap {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 100vh;
    height: 100dvh;
    background-color: #000;
  }
  .home-video {
    /* PHASE 7 CORRECTION: hero-loop-mobile.mp4 is a fixed 720x1280 (9:16,
       ratio 0.5625) shot with the logo composed for that exact frame — but
       real phone viewports are taller than 9:16 (an iPhone at 375x812 is
       ratio ~0.462, many Android phones are similar or taller). With
       object-fit:cover, the video is scaled up to fill that extra height,
       which pushes part of the logo lockup off both left and right edges —
       confirmed live at 375x812 (part of the "AI IMAGE & VIDEO" wordmark
       and the icon get cropped). object-fit:contain always shows the full
       frame (letterboxed top/bottom against this element's own black
       background, matching the video's own black), so the logo is never
       clipped regardless of how tall/narrow the real device viewport is —
       this is what the "no essential hero content may be clipped" rule
       requires. Revert to cover only if a future re-export of this asset is
       actually composed to a ratio that matches real device viewports. */
    top: 0; right: 0; bottom: 0; left: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    object-position: center center;
    background: #000;
  }
  .home-enter-btn {
    bottom: 8%; left: 50%;
    transform: translateX(-50%);
    padding: 14px 38px;
    font-size: 0.875rem;
  }
  .home-enter-btn:active { transform: translateX(-50%) translateY(1px); }
}

.rail { min-width: 0; }
.rail-brand > div { min-width: 0; }


.view { display: none; }
.view.active { display: block; }

.module-panel { display: none; }
.module-panel.active { display: block; }

.panel-head { margin-bottom: 28px; }
.panel-head-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }

/* "Character Sheet →" nav link on the Create Your Character page, restyled
   from a small inline text link into a prominent pill button. Same
   destination/behavior (data-nav="charsheet") — visual only. */
.charsheet-pill-link {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: 18px;
  padding: 14px 26px;
  border-radius: 999px;
  background: linear-gradient(180deg, #232323 0%, #0a0a0a 100%);
  border: 1px solid var(--accent);
  color: #ffffff;
  font-family: var(--font-display);
  font-size: 0.9375rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-decoration: none;
  cursor: pointer;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 0 22px rgba(0, 0, 0, 0.22);
  transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.charsheet-pill-arrow { transition: transform 0.18s ease; }
.charsheet-pill-link:hover {
  background: linear-gradient(180deg, #2f2f2f 0%, #141414 100%);
  border-color: var(--accent);
  color: #ffffff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.16), 0 0 32px rgba(0, 0, 0, 0.38);
  transform: translateY(-1px);
}
.charsheet-pill-link:hover .charsheet-pill-arrow { transform: translateX(3px); }
.charsheet-pill-link:active { transform: translateY(0); }

.link-btn {
  background: none; border: none; color: var(--text-faint);
  font-family: var(--font-mono); font-size: 0.75rem; cursor: pointer;
  padding: 0; margin-bottom: 8px;
}
.link-btn:hover { color: var(--text-muted); }

.module-switch { display: inline-flex; gap: 3px; background: var(--surface-2); border: 1px solid var(--border-soft); padding: 3px; border-radius: 999px; }
.module-btn {
  background: transparent; border: none; color: var(--text-muted); padding: 7px 16px;
  border-radius: 999px; font-family: var(--font-body); font-size: 0.8125rem; font-weight: 600; cursor: pointer;
}
.module-btn.active { background: var(--accent); color: #ffffff; }

.segmented { display: inline-flex; gap: 3px; background: var(--surface-2); border: 1px solid var(--border-soft); padding: 3px; border-radius: var(--radius); }
.seg-btn {
  background: transparent; border: none; color: var(--text-muted); padding: 6px 14px;
  border-radius: 6px; font-family: var(--font-mono); font-size: 0.8125rem; cursor: pointer; min-width: 34px;
  display: flex; align-items: center; justify-content: center;
}
.seg-btn.active { background: var(--accent); color: #ffffff; }

/* ---------- card grid (projects / characters / explore) ---------- */

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}

.entity-card {
  background: var(--panel-sheen), var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  cursor: pointer;
  position: relative;
  transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

/* Small, unobtrusive mode indicator (Enhance page's shared History — labels
   a card "Remaster" or "2× Upscale") — no large badge, just a quiet tag. */
.entity-card-tag {
  position: absolute; top: 8px; left: 8px;
  background: rgba(0, 0, 0, 0.65); color: #fff;
  font-family: var(--font-mono); font-size: 0.625rem; letter-spacing: 0.02em;
  padding: 2px 7px; border-radius: 999px; pointer-events: none;
}
.entity-card[data-noclick] { cursor: default; }
.entity-card:hover {
  border-color: rgba(0, 0, 0, 0.4);
  transform: translateY(-3px);
  box-shadow: var(--shadow-glow);
}

.entity-card .thumb {
  width: 100%; height: 150px; object-fit: cover; display: block; background: var(--surface-2);
  transition: transform 0.35s ease;
}
.entity-card:hover .thumb { transform: scale(1.05); }
.entity-card .thumb-empty {
  width: 100%; height: 150px; display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--text-faint); font-size: 1.375rem;
}
/* A job still queued/running server-side — possibly started from another
   device or a since-closed tab — shown in History as "still coming" rather
   than the plain empty-diamond "nothing here" state. See
   isVisibleInHistory/schedulePendingHistoryRefresh in app.js. */
.entity-card .thumb-pending { font-size: 0; }
.thumb-pending-spinner {
  width: 22px; height: 22px; border-radius: 50%;
  border: 2px solid var(--text-faint); border-top-color: transparent;
  animation: thumb-pending-spin 0.8s linear infinite;
}
@keyframes thumb-pending-spin { to { transform: rotate(360deg); } }
.entity-card .meta { padding: 12px 14px; }
.entity-card .entity-name { font-family: var(--font-display); font-weight: 700; font-size: 0.875rem; margin-bottom: 3px; }
.entity-card .entity-sub { font-family: var(--font-mono); font-size: 0.6875rem; color: var(--text-faint); }

/* Media-only cards (Image/Video/Character Sheet/Scene history, Projects):
   no visible title/metadata/status text on the card face — the tile ends
   exactly where the thumb ends. Action buttons (Publish/Download/Delete/
   Reuse/etc.) still work; they're just hidden until hover/focus instead of
   always taking up footer space, so a card with a thumb but no visible
   label doesn't look like it's missing something. */
.entity-card-media-only .entity-card-actions {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  margin: 0;
  padding: 8px 10px;
  background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.75) 60%);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.15s ease, transform 0.15s ease;
  pointer-events: none;
}
.entity-card-media-only { position: relative; }
/* A job still generating has nothing to hover over yet (no thumbnail) and
   the person may genuinely be trying to figure out if it's stuck — the
   Cancel action on a pending card (see jobCardHtml/sceneHistoryEntryHtml)
   stays visible always, unlike the hover-revealed actions on a finished
   card above it. */
.entity-card-media-only .entity-card-actions-pending {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  position: static;
  background: none;
  display: flex;
  justify-content: center;
  padding-top: 10px;
}
.job-cancel-btn {
  background: var(--surface-2);
  border: 1px solid var(--text-faint);
  border-radius: 6px;
  padding: 4px 12px;
  font-size: 0.75rem;
  cursor: pointer;
}
.entity-card-media-only:hover .entity-card-actions,
.entity-card-media-only:focus-within .entity-card-actions {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

/* ---------- global image tags (shared everywhere an image/video asset
   is shown — cards, Elements, the lightbox) — see imageTagsRowHtml() in
   app.js. Deliberately small/quiet: a media-only card (History/Projects/
   Workspace/etc.) must still "end where the thumb ends" by default, so this
   overlays at the TOP of the card the same hover/focus-reveal way the
   existing bottom actions overlay already works, rather than adding a
   permanent extra row that would change card height/grid spacing. ---------- */
.entity-card-media-only .image-tags-row {
  position: absolute;
  left: 0; right: 0; top: 0;
  margin: 0;
  padding: 8px 10px 16px;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.7), transparent 70%);
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.15s ease, transform 0.15s ease;
  pointer-events: none;
  display: flex; align-items: flex-start; gap: 6px; flex-wrap: wrap;
}
.entity-card-media-only:hover .image-tags-row,
.entity-card-media-only:focus-within .image-tags-row {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.image-tags-row {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
.image-tags-list { display: flex; flex-wrap: wrap; gap: 5px; }
.image-tag-chip {
  display: inline-flex; align-items: center; gap: 4px;
  font-family: var(--font-mono); font-size: 0.65625rem; letter-spacing: 0.3px;
  text-transform: uppercase; color: #fff;
  background: rgba(0, 0, 0, 0.55); border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 20px; padding: 2px 8px;
  white-space: nowrap;
}
.image-tag-chip-remove {
  appearance: none; border: none; background: none; color: inherit;
  font-size: 0.6875rem; line-height: 1; cursor: pointer; padding: 0; opacity: 0.7;
}
.image-tag-chip-remove:hover { opacity: 1; }
.image-tag-add-btn {
  appearance: none; border: 1px dashed rgba(255, 255, 255, 0.35); background: none;
  color: #fff; font-family: var(--font-mono); font-size: 0.65625rem; letter-spacing: 0.3px;
  text-transform: uppercase; border-radius: 20px; padding: 2px 8px; cursor: pointer;
  opacity: 0.75;
}
.image-tag-add-btn:hover { opacity: 1; }
/* Elements grid + the lightbox aren't media-only overlay cards — tags sit
   in normal flow there, right where the existing category/name line
   already is, using dark-on-panel colors instead of the white-on-photo
   overlay palette above. */
.elements-card .image-tags-row { margin-top: 6px; }
.elements-card .image-tag-chip { color: var(--text); background: var(--vf-surface-2); border-color: var(--border); }
.elements-card .image-tag-add-btn { color: var(--text-muted); border-color: var(--border); }
.image-lightbox-tags {
  position: absolute; left: 50%; bottom: 18px; transform: translateX(-50%);
  max-width: 90vw; z-index: 2;
}
.image-lightbox-tags .image-tag-chip { background: rgba(20, 20, 20, 0.85); }

.entity-card.new-tile {
  display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 8px;
  height: 100%; min-height: 196px; border-style: dashed; color: var(--text-muted);
}
.entity-card.new-tile .plus { font-size: 1.375rem; color: var(--accent); }
.entity-card.new-tile:hover { color: var(--text); border-color: var(--accent); }

.new-character-panel { max-width: 560px; }
.new-character-panel h3 { font-family: var(--font-display); font-size: 1rem; margin: 0 0 16px; }

/* ---------- eyebrow label (panel-head) ---------- */

.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--accent);
  text-shadow: 0 0 14px rgba(0, 0, 0, 0.35);
  margin: 0 0 8px;
}

/* ---------- projects grid ---------- */

.projects-grid {
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

.projects-card {
  position: relative;
  aspect-ratio: 3 / 4;
  height: auto;
  background-color: var(--surface);
  background-size: cover;
  background-position: center;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.projects-card:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-glow);
}

.projects-card-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-faint);
  font-size: 1.875rem;
  background: var(--surface-2);
}

.projects-card-label {
  position: relative;
  z-index: 1;
  padding: 12px 14px 14px;
  background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.78) 70%);
}

.projects-card-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.875rem;
  color: #fff;
  margin-bottom: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.projects-card-count {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: rgba(255, 255, 255, 0.72);
}

.projects-new-tile {
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--accent);
  gap: 10px;
  color: var(--text-muted);
  aspect-ratio: auto;
  min-height: 320px;
  position: relative;
  background: var(--surface-2);
}
.projects-new-tile::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: radial-gradient(var(--black-dots) 1px, transparent 1px);
  background-size: 18px 18px;
  pointer-events: none;
}
.projects-new-tile > * { position: relative; z-index: 1; }
.projects-new-tile:hover {
  border-color: var(--accent);
  color: var(--text);
  box-shadow: 0 0 24px -6px rgba(0, 0, 0, 0.35);
}

/* ---------- projects workspace: dotted canvas, toolbar, empty state ---------- */

/* ---------- Shared Ember background system ----------
   One dot recipe (spacing/size/warm tint/fade) used everywhere so every
   workspace reads as the same environment. Only the amber atmosphere's
   intensity changes: creation/discovery pages (Studio, Explore, Projects,
   Elements, Image, Video, Scene, Characters, Character Sheet, Workflow)
   get a noticeable-but-restrained glow; functional/account pages (Admin,
   Settings, Profile) keep the same dots but a much quieter glow so they
   don't distract from tables and forms. Never applied to .rail (sidebar). */
.projects-canvas, .elements-canvas, .admin-canvas, .settings-canvas, .profile-canvas, .explore-canvas, .vault-canvas { position: relative; }
.projects-canvas::before, .elements-canvas::before, .admin-canvas::before, .settings-canvas::before, .profile-canvas::before, .explore-canvas::before, .vault-canvas::before {
  content: '';
  position: absolute;
  inset: -8px;
  border-radius: var(--radius-lg);
  background-image: radial-gradient(var(--black-dots) 1px, transparent 1px);
  background-size: 22px 22px;
  pointer-events: none;
  z-index: 0;
}
.projects-canvas > *, .elements-canvas > *, .admin-canvas > *, .settings-canvas > *, .profile-canvas > *, .explore-canvas > *, .vault-canvas > * { position: relative; z-index: 1; }

/* ---------- Explore: wide workspace, category pills, gallery cards ---------- */

/* The dot grid used to live on a `::before` pseudo-element sized with
   `position:absolute; inset:0`. That pseudo is a real box, not a paint
   layer, so its height resolved against main's own explicit `height:100vh`
   (main scrolls its content internally — see the `@media (min-width:861px)`
   block above) rather than the full scrollable content height. The result:
   the dot grid only ever covered the first viewport-height of the page and
   then scrolled away with the content, leaving every row/category below
   that point on a flat, dot-less background.
   Fix: fold the dot grid into main's own `background` property instead of a
   separate positioned element. A background layer's default
   `background-attachment: scroll` ties it to the element's own box/viewport
   (not to its scrollable content), so a repeating pattern like this one
   stays visibly tiled across the ENTIRE scrollable area no matter how tall
   the content grows — exactly how the two scanline layers below it already
   behave. No pseudo-element, no height dependency, nothing to outgrow. */
main:has(> #view-explore.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-explore .explore-canvas::before { display: none; }
#view-explore.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}

.explore-category-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 24px;
}
.explore-cat-pill {
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 0.71875rem;
  font-weight: 600;
  letter-spacing: 0.4px;
  padding: 7px 16px;
  border-radius: 999px;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.explore-cat-pill:hover:not(.active) { border-color: var(--accent); color: var(--text); }
.explore-cat-pill.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #ffffff;
}

/* Each category is its own independent horizontal carousel — rows stack
   vertically, but every row's track scrolls on its own axis. */
#explore-rows { display: flex; flex-direction: column; gap: 34px; }
.explore-row-head {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: 14px;
}
.explore-row-icon { font-size: 1rem; color: var(--accent); line-height: 1; }
.explore-row-head h3 {
  margin: 0; font-family: var(--font-display); font-size: 0.9375rem; font-weight: 700; color: var(--text);
  letter-spacing: 0.2px;
}

/* Prev/track/next sit side by side as ordinary flex siblings — not
   absolutely stacked on top of the track — so the arrows get their own
   dedicated gutter and can never overlap the first/last card's media. */
.explore-row-carousel {
  display: flex;
  align-items: center;
  gap: 22px;
}
.explore-row-track {
  display: flex;
  flex-wrap: nowrap;
  flex: 1 1 auto;
  min-width: 0;
  gap: 18px;
  overflow-x: auto;
  scroll-behavior: smooth;
  padding-bottom: 4px;
  cursor: grab;
  /* Fade the row's right edge so the next, partially-visible card reads as
     "more content" rather than a hard crop. */
  -webkit-mask-image: linear-gradient(90deg, black calc(100% - 48px), transparent 100%);
  mask-image: linear-gradient(90deg, black calc(100% - 48px), transparent 100%);
}
.explore-row-track.is-dragging { cursor: grabbing; }
.explore-row-track::-webkit-scrollbar { height: 0; }

/* ~4 cards visible on a wide desktop viewport, with the next one peeking
   in — fixed width rather than grid so the row can overflow horizontally. */
.explore-card {
  flex: 0 0 280px;
  width: 280px;
  background: var(--panel-sheen), var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.explore-card:hover {
  border-color: rgba(0, 0, 0, 0.4);
  transform: translateY(-3px);
  box-shadow: var(--shadow-glow);
}
.explore-card-media { width: 100%; background: var(--surface-2); overflow: hidden; position: relative; }
.explore-card-media .thumb { width: 100%; height: 100%; object-fit: cover; display: block; }
.explore-card-media .thumb-empty {
  width: 100%; aspect-ratio: 4 / 3; display: flex; align-items: center; justify-content: center;
  color: var(--text-faint); font-size: 1.5rem;
}
/* Image tiles: the image itself is the click target (whole-tile click, no
   separate button — per the Explore spec, every image can be clicked to
   enlarge it). Video tiles: the <video> thumbnail is a static, inert
   preview only (pointer-events: none) — the single visible Play button
   below is the ONLY way to open/play it, so there is exactly one control
   per video tile, never two overlapping click targets. */
.explore-card-media img.thumb { cursor: pointer; }
.explore-card-media video.thumb { pointer-events: none; }

/* The one allowed control on a video tile. Always visible (not hover-only)
   per spec, centered over the thumbnail, and does nothing but open the
   enlarged viewer and start playback — no other icons/menus are added
   next to it. */
.explore-play-btn {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 54px; height: 54px; border-radius: 50%;
  background: rgba(20, 18, 16, 0.72); border: 1.5px solid rgba(255, 255, 255, 0.85);
  color: #fff; display: flex; align-items: center; justify-content: center;
  cursor: pointer; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
  transition: background 0.15s ease, transform 0.15s ease;
  -webkit-tap-highlight-color: transparent;
}
.explore-play-btn::after {
  content: ''; width: 0; height: 0;
  border-style: solid; border-width: 9px 0 9px 15px;
  border-color: transparent transparent transparent #fff;
  margin-left: 3px;
}
.explore-play-btn:hover { background: rgba(20, 18, 16, 0.9); transform: translate(-50%, -50%) scale(1.06); }
.explore-play-btn:active { transform: translate(-50%, -50%) scale(0.96); }
/* Aspect ratio varies by content type so different media keeps a useful
   shape instead of every card forcing an identical square thumbnail. */
.explore-card-image .explore-card-media, .explore-card-scene .explore-card-media { aspect-ratio: 3 / 2; }
.explore-card-video .explore-card-media { aspect-ratio: 16 / 9; }
.explore-card-charsheet .explore-card-media { aspect-ratio: 16 / 8; }
.explore-card-characters .explore-card-media { aspect-ratio: 3 / 4; }
/* Character headshots are portrait, so at the standard 280px card width a
   3:4 box runs much taller than every other row's cards. Narrow just this
   category's card so its footprint stays in proportion to its neighbors. */
.explore-card-characters { flex: 0 0 180px; width: 180px; }

.explore-card-body { padding: 12px 14px 4px; }
.explore-card-title {
  font-family: var(--font-display); font-weight: 700; font-size: 0.84375rem; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 4px;
}
.explore-card-meta {
  font-family: var(--font-mono); font-size: 0.65625rem; color: var(--text-faint);
}
.explore-card-actions {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 10px 14px 14px;
  margin-top: auto;
}
.explore-published-tag {
  background: var(--success-soft); border: none; color: var(--success);
  font-family: var(--font-mono); font-size: 0.625rem; font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
  padding: 4px 10px; border-radius: 999px; cursor: pointer;
}
.explore-published-tag:hover { filter: brightness(1.15); }

.explore-row-empty {
  flex: 1 1 auto;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; gap: 8px;
  padding: 32px 20px;
  min-height: 140px;
  width: 100%;
}
.explore-row-empty .projects-empty-title { font-family: var(--font-display); font-size: 0.9375rem; font-weight: 700; color: var(--text); margin: 0; }

/* Left/right chevrons sit beside the row as their own flex column, fully
   outside the track's box — never layered over the first/last card. Prev
   only appears once the row has scrolled away from the start; next hides
   once fully scrolled; when a button is hidden the gutter it occupied
   collapses so the track reclaims that space. */
.explore-row-nav {
  flex: 0 0 auto;
  align-self: center;
  width: 36px;
  height: 96px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10, 10, 12, 0.7);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 1.125rem;
  cursor: pointer;
  border-radius: var(--radius);
  transition: background 0.15s ease, border-color 0.15s ease;
}
.explore-row-nav:hover { background: rgba(0, 0, 0, 0.18); border-color: var(--accent); }
.explore-row-nav[hidden] { display: none; }
@media (max-width: 720px) { .explore-row-nav { display: none; } }

/* Projects page: same wide-and-centered treatment as Elements/Admin/
   Settings, so the header toolbar (search/sort/view-toggle) sits anchored
   to the true right edge of the workspace instead of a narrower, left-hung
   1180px column, and the bottom info strip — full width of this now-
   centered container — reads as centered in the usable content area. */
/* Full-bleed brushed-metal + dot background: main itself becomes the
   background layer, spanning the true workspace edge-to-edge (sidebar to
   far right, top to bottom of the page) — independent of the centered
   1600px content column, which is re-applied directly on #view-projects
   below so the actual content renders at exactly the same position/size
   as before. */
main:has(> #view-projects.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  /* Restrained brushed-aluminum: fine directional streaks layered over the
     app's exact metallic gradient — brighter/darker banding, not gloss. */
  background:
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
}
/* Dot grid painted on this same full-bleed layer — uniform, no mask/fade,
   spans the entire main box (which extends to the full scrollable content
   height, not just the viewport). */
main:has(> #view-projects.active)::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: radial-gradient(var(--black-dots) 1px, transparent 1px);
  background-size: 22px 22px;
  pointer-events: none;
  z-index: 0;
}
/* The old canvas-scoped dot layer (shared with Elements/Explore/Admin/etc.)
   is replaced by the full-bleed one above for this page only. */
#view-projects .projects-canvas::before { display: none; }
/* Footer-style placement: the view becomes a flex column at least one
   viewport tall, and the info strip is pushed to the bottom of that column
   with margin-top:auto — so on a normal-length page it lands near the
   bottom of the viewport, roughly level with the sidebar's account/profile
   block, instead of floating right under the content. On a long/scrolled
   page it simply sits after the content, same as before. Padding/max-width/
   centering that used to live on `main` (now full-bleed above) is
   reapplied here so the content renders at the exact same position/size. */
#view-projects.active {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  min-height: 100vh;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}
#view-projects .info-strip { margin-top: auto; padding-top: 64px; }

/* ---------- elements library page ---------- */

/* Full-bleed brushed-metal + dot background: main itself becomes the
   background layer, spanning the true workspace edge-to-edge (sidebar to
   far right, top to bottom of the page) — independent of the centered
   1600px content column, which is re-applied directly on #view-elements
   below so the actual content renders at exactly the same position/size
   as before. Same technique used for Projects. */
main:has(> #view-elements.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  /* Dots folded directly into main's own background (not a ::before
     pseudo) so the pattern tiles across the full scrollable height
     instead of only the first viewport — see #view-explore's fix above. */
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
/* The old canvas-scoped dot layer (shared with Projects/Explore/Admin/etc.)
   is replaced by the full-bleed one above for this page only. */
#view-elements .elements-canvas::before { display: none; }
#view-elements.active {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  min-height: 100vh;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}
#view-elements .info-strip { margin-top: auto; padding-top: 64px; }

/* ---------- moodboards page ---------- */
/* Same full-bleed brushed-metal + dot background technique as Elements
   above: main itself becomes the background layer spanning the true
   workspace edge-to-edge and for the full scrollable height, independent
   of the centered 1600px content column (re-applied on #view-moodboards
   below). Without this, #view-moodboards fell back to the old
   canvas-scoped `.elements-canvas::before` layer, which is a positioned
   box sized against its own content height rather than main's full
   scrollable height, leaving dot-less bands below the fold. */
main:has(> #view-moodboards.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  background:
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
}
main:has(> #view-moodboards.active)::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: radial-gradient(var(--black-dots) 1px, transparent 1px);
  background-size: 22px 22px;
  pointer-events: none;
  z-index: 0;
}
/* The old canvas-scoped dot layer (shared with Projects/Explore/Admin/etc.)
   is replaced by the full-bleed one above for this page only. */
#view-moodboards .elements-canvas::before { display: none; }
#view-moodboards.active {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  min-height: 100vh;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}
#view-moodboards .info-strip { margin-top: auto; padding-top: 64px; }

/* ---------- collections page (Favorites + Collections) ---------- */
/* Same full-bleed brushed-metal + dot background technique as Elements/
   Moodboards above. Collections previously kept only the old canvas-scoped
   `.elements-canvas::before` dot layer (a positioned box sized against its
   own content height, and never given a widened content column at all) —
   the page fell back to `main`'s default 1180px max-width with no
   full-bleed override, which is why it looked like a small block of
   content stranded in the upper-left of a much larger, mostly dot-less
   page. This makes it match every other dotted workspace page exactly. */
main:has(> #view-collections.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
/* The old canvas-scoped dot layer is replaced by the full-bleed one above
   for this page only — the wrapper div keeps its class (nothing in app.js
   references it, so this is layout-only) but its own dot paint is turned
   off to avoid a doubled/misaligned pattern underneath the new one. */
#view-collections .elements-canvas::before,
#view-collections .projects-canvas::before { display: none; }
#view-collections.active {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  min-height: 100vh;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}
/* The inner canvas wrapper no longer needs to constrain width itself — the
   centering/max-width now lives on #view-collections.active above, same
   split used by Elements/Moodboards (full-bleed background on `main`,
   centered content column on the view). */
#view-collections .projects-canvas { width: 100%; }

/* Vertical rhythm between header / Favorites / Your Collections — each
   section is its own clearly separated block instead of everything
   crowding against the header. */
#view-collections .collections-section { margin-top: 36px; }
#view-collections .collections-section:first-of-type { margin-top: 28px; }
#view-collections .collections-section .section-label-row { margin-bottom: 14px; }

/* A Favorites/Collections empty state used to be two bare <p> tags dropped
   directly into the auto-fill grid — each one became its own narrow grid
   cell (since minmax(200px, 1fr) applies to EVERY direct child, including
   plain text), which is exactly why the title and helper text ended up as
   two tiny separate columns bunched near the far left instead of reading
   as one message. Spanning the full grid width and centering within that
   span fixes this without touching the grid itself (real cards still flow
   into normal columns once any exist). */
.collections-empty-block {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 8px;
  max-width: 480px;
  margin: 0 auto;
  padding: 48px 24px;
}
.collections-empty-block .projects-empty-title { margin: 0; }
.collections-empty-block .section-hint { margin: 0; }

@media (max-width: 900px) {
  #view-collections.active { padding: 24px 16px 40px; }
}

.elements-lib-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }

.elements-page-new-tile { aspect-ratio: 3 / 4; height: auto; min-height: 0; }

.elements-page-card {
  display: flex;
  flex-direction: column;
}
.elements-page-card-thumb {
  flex: 1;
  background-size: cover;
  background-position: center;
  background-color: var(--surface-2);
}
.elements-page-card-thumb-empty {
  display: flex; align-items: center; justify-content: center;
  color: var(--text-faint); font-size: 1.625rem;
}
.elements-page-card-label { padding: 10px 12px 12px; background: var(--surface); }
.elements-page-card-tag {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.84375rem;
  color: var(--accent);
  line-height: 1.3;
  word-break: break-word;
  margin-bottom: 3px;
}
.elements-page-card-meta {
  display: flex; justify-content: space-between; gap: 8px;
  font-size: 0.6875rem; color: var(--text-faint); font-family: var(--font-mono);
}
/* Which project an element belongs to. The library spans all of them now, so
   without this two identically-named references from different projects are
   indistinguishable on the page. */
.elements-page-card-project {
  margin-top: 4px;
  font-size: 0.625rem; letter-spacing: 0.04em; text-transform: uppercase;
  font-family: var(--font-mono); color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.elements-page-card-project.is-orphan { opacity: 0.6; font-style: italic; }
.elements-page-card-menu {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 26px; height: 26px; border-radius: 999px;
  background: rgba(20, 20, 23, 0.65); border: 1px solid var(--border); color: var(--text-muted);
  display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 0.875rem; line-height: 1;
}
.elements-page-card-menu:hover { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }

.projects-grid.list-view .elements-page-card-thumb { flex: 0 0 72px; height: 72px; }
.projects-grid.list-view .elements-page-card-label {
  flex: 1; padding: 0 14px; background: none;
  display: flex; flex-direction: column; justify-content: center;
}
.projects-grid.list-view .elements-page-card-menu { position: static; margin-left: auto; margin-right: 10px; }

.elements-filter-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.elements-filter-chip {
  background: var(--surface-2); border: 1px solid var(--border-soft); color: var(--text-muted);
  font-family: var(--font-mono); font-size: 0.71875rem; font-weight: 600; letter-spacing: 0.3px;
  padding: 6px 14px; border-radius: 999px; cursor: pointer;
}
.elements-filter-chip.active { background: var(--accent); color: #ffffff; border-color: var(--accent); }
.elements-filter-chip:hover:not(.active) { border-color: var(--accent); color: var(--text); }

.elements-grow-hint {
  margin-top: 18px;
  padding: 34px 20px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; gap: 10px;
}
.elements-grow-hint .projects-empty-glyph { font-size: 1.875rem; opacity: 0.7; }
.elements-grow-hint .projects-empty-title { font-family: var(--font-display); font-weight: 700; font-size: 1.0625rem; color: var(--vf-text); margin: 0; }
.elements-grow-hint .section-hint { max-width: 460px; margin: 0 auto; }

.projects-head-row { align-items: flex-start; }

.projects-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.projects-search { position: relative; }
.projects-search input {
  width: 220px;
  padding: 9px 12px 9px 38px;
}
.projects-search-icon {
  position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
  color: var(--text-faint); font-size: 0.8125rem; line-height: 1; pointer-events: none;
}
.projects-sort-select { width: auto; min-width: 140px; padding: 9px 30px 9px 12px; font-size: 0.78125rem; }
.projects-view-toggle .seg-btn { font-size: 0.9375rem; padding: 6px 12px; }

.projects-grid.is-empty {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: 24px;
  min-height: 320px;
}
.projects-grid.is-empty .projects-new-tile { flex: 0 0 300px; }

.projects-empty {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: max-content;
  max-width: calc(100% - 40px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 10px;
  padding: 40px 20px;
  cursor: default;
  /* Purely decorative placeholder text — it has no click behavior of its
     own (see `data-noclick`, which only ever changed the cursor, never
     actually stopped clicks from landing on this element). Centered
     absolutely over the WHOLE grid container, it sits on top of the New
     Project / New Element tile in DOM stacking order, so without this a
     tap square on the visible "New Project" button was actually being
     caught by this invisible-but-present layer instead — the real cause
     of "the New Project button isn't working". `pointer-events:none` lets
     every tap pass straight through to whatever's underneath it. */
  pointer-events: none;
}
/* On narrow/mobile widths the New Project/New Element tile's own fixed
   300px basis fills the whole (much narrower) container, so this
   message's absolute-centered-over-everything positioning landed it
   directly ON TOP of the tile's own text instead of beside it — the
   garbled overlapping "New Project / Your projects will live here" text.
   Below 860px this switches to a normal stacked, in-flow layout instead:
   tile first, placeholder text below it, no overlap possible regardless
   of container width. */
@media (max-width: 860px) {
  .projects-grid.is-empty {
    flex-direction: column;
    align-items: center;
    min-height: 0;
  }
  .projects-grid.is-empty .projects-new-tile {
    flex: 0 0 auto;
    width: 100%;
    max-width: 300px;
  }
  .projects-grid.is-empty .projects-empty {
    position: static;
    left: auto; top: auto;
    transform: none;
    width: 100%;
    max-width: none;
    padding: 24px 20px 8px;
  }
}
.projects-empty-glyph { font-size: 2rem; color: var(--text-faint); opacity: 0.75; }
.projects-empty-title { font-family: var(--font-display); font-size: 1.125rem; font-weight: 700; color: var(--text); margin: 0; }
.projects-empty .section-hint { max-width: 420px; margin: 0; }

/* list view: compact full-width rows instead of portrait cards */
.projects-grid.list-view {
  grid-template-columns: 1fr;
  gap: 10px;
}
.projects-grid.list-view .projects-card,
.projects-grid.list-view .projects-new-tile {
  aspect-ratio: auto;
  min-height: 0;
  height: 72px;
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
}
.projects-grid.list-view .projects-card-label {
  width: 100%;
  padding: 0 16px;
  background: linear-gradient(90deg, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0.28));
}
.projects-grid.list-view .projects-new-tile { padding-left: 22px; gap: 14px; }
.projects-grid.list-view .projects-new-plus { margin-bottom: 0; width: 36px; height: 36px; font-size: 1.125rem; }

/* ---------- bottom info strip ---------- */

.info-strip {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
  margin-top: 36px;
  padding: 24px 28px;
  background: var(--panel-sheen), var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.info-strip-item { display: flex; gap: 14px; align-items: flex-start; }
.info-strip-icon {
  flex: 0 0 auto;
  width: 38px; height: 38px;
  border-radius: var(--radius);
  background: var(--accent-soft);
  color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 1rem;
}
.info-strip-body h4 { margin: 0 0 4px; font-family: var(--font-display); font-size: 0.875rem; font-weight: 700; color: var(--text); }
.info-strip-body p { margin: 0; font-size: 0.78125rem; color: var(--text-faint); line-height: 1.5; }
@media (max-width: 900px) { .info-strip { grid-template-columns: 1fr; } }
@media (max-width: 640px) { .projects-search input { width: 150px; } }

.publish-btn { width: auto; padding: 0 10px; font-size: 0.6875rem; }
.publish-btn.published { color: var(--success); border-color: var(--success); }

.projects-card-delete {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: rgba(20, 20, 23, 0.72);
  border: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 0.8125rem;
  cursor: pointer;
  line-height: 1;
  /* Hidden until the card is hovered/focused — the label it used to sit
     alongside is gone, so it no longer needs to be visible by default. */
  opacity: 0;
  transition: opacity 0.15s ease, background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.projects-card:hover .projects-card-delete,
.projects-card:focus-within .projects-card-delete {
  opacity: 1;
}
.projects-card-delete:hover {
  background: var(--danger-soft);
  border-color: var(--danger);
  color: var(--danger);
}

.projects-new-plus {
  width: 44px; height: 44px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.375rem; font-weight: 700;
  margin-bottom: 4px;
}

.projects-new-label {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.875rem;
  color: var(--text);
}

.projects-new-sub {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--text-faint);
}

.history-block { margin-top: 28px; }
.history-block h3 { font-family: var(--font-display); font-size: 1rem; margin: 0 0 14px; }

/* ---------- dashboard hero (Projects landing) ---------- */

.dash-layout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 24px; align-items: start; }
@media (max-width: 1100px) { .dash-layout { grid-template-columns: minmax(0, 1fr); } }

.dash-heading { margin-bottom: 22px; }
.dash-heading .dash-kicker {
  font-family: var(--font-body); font-weight: 700; font-size: 0.8125rem; color: var(--accent);
  text-transform: uppercase; letter-spacing: 0.6px; margin: 6px 0 0;
}
.dash-heading h1 {
  font-family: var(--font-display); font-weight: 800; font-size: 2.125rem; line-height: 1.05;
  text-transform: uppercase; letter-spacing: 0.2px; margin: 0;
}

.hero-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 18px; margin-bottom: 22px; }
@media (max-width: 700px) { .hero-row { grid-template-columns: minmax(0, 1fr); } }

.hero-card {
  text-decoration: none;
  position: relative;
  border-radius: var(--radius-lg);
  padding: 26px;
  min-height: 230px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  overflow: hidden;
  border: 1px solid var(--border);
  cursor: pointer;
  isolation: isolate;
}
.hero-card::before, .hero-card::after {
  content: ''; position: absolute; width: 26px; height: 26px; pointer-events: none; z-index: 2;
}
.hero-card::before { top: 12px; left: 12px; border-top: 2px solid; border-left: 2px solid; border-color: inherit; }
.hero-card::after { top: 12px; right: 12px; border-top: 2px solid; border-right: 2px solid; border-color: inherit; }

.hero-card .hero-scene {
  position: absolute; inset: 0; z-index: -2;
  background-size: cover;
  background-position: center;
}
.hero-card .hero-scrim {
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(90deg, var(--bg) 0%, rgba(10,10,12,0.82) 30%, rgba(10,10,12,0.25) 62%, rgba(10,10,12,0.08) 100%),
              linear-gradient(0deg, rgba(10,10,12,0.35) 0%, transparent 40%);
}

.hero-card.hero-teal { color: var(--teal); }
.hero-card.hero-teal .hero-scene {
  background:
    linear-gradient(90deg, rgba(14,35,33,0.75) 0%, rgba(14,35,33,0.15) 45%, transparent 70%),
    url('/images/hero-image.jpg');
  background-size: cover;
  background-position: center;
}
.hero-card.hero-accent { color: var(--accent); }
.hero-card.hero-accent .hero-scene {
  background:
    linear-gradient(90deg, rgba(36,22,16,0.75) 0%, rgba(36,22,16,0.15) 45%, transparent 70%),
    url('/images/hero-video.jpg');
  background-size: cover;
  background-position: center;
}

.hero-card h2 {
  font-family: var(--font-display); font-weight: 800; font-size: 1.5625rem; color: var(--text);
  margin: 0 0 6px; text-transform: uppercase; letter-spacing: 0.3px; line-height: 1.08;
}
.hero-card p { color: var(--text-muted); font-size: 0.8125rem; margin: 0 0 16px; max-width: 32ch; text-transform: uppercase; letter-spacing: 0.4px; font-weight: 600; }
.hero-card .hero-cta {
  align-self: flex-start; display: inline-flex; align-items: center; gap: 6px;
  padding: 10px 18px; border-radius: 7px; font-weight: 700; font-size: 0.8125rem;
  border: none; cursor: pointer; font-family: var(--font-body); color: var(--bg);
}
.hero-card.hero-teal .hero-cta { background: var(--teal); }
.hero-card.hero-accent .hero-cta { background: var(--accent); }

.section-label {
  font-family: var(--font-mono); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.8px;
  color: var(--text-label); margin: 0 0 12px;
}

.quick-tools-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: 28px;
}
.quick-tool {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: 16px 12px; text-align: center; cursor: pointer; color: var(--text-muted);
}
.quick-tool:hover { border-color: var(--tool-color, var(--accent)); }
.quick-tool .qt-icon-box {
  width: 40px; height: 40px; border-radius: 10px; margin: 0 auto 10px;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--tool-color, var(--accent)) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--tool-color, var(--accent)) 35%, transparent);
}
.quick-tool .qt-icon { font-size: 1rem; color: var(--tool-color, var(--accent)); }
.quick-tool .qt-label { font-family: var(--font-body); font-size: 0.78125rem; font-weight: 700; color: var(--text); }
.quick-tool .qt-sub { font-size: 0.6875rem; color: var(--text-faint); margin-top: 2px; }

.recent-panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 18px; }
.recent-panel-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
.recent-panel h3 {
  font-family: var(--font-mono); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.8px;
  color: var(--text-faint); margin: 0;
}
.recent-item { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--border-soft); }
.recent-item:last-child { border-bottom: none; }
.recent-item .recent-thumb {
  width: 42px; height: 42px; border-radius: 6px; object-fit: cover; background: var(--surface-2); flex-shrink: 0;
  border-left: 2px solid var(--item-color, var(--text-faint));
}
.recent-item .recent-meta { min-width: 0; }
.recent-item .recent-name { font-size: 0.78125rem; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-transform: uppercase; letter-spacing: 0.2px; }
.recent-item .recent-sub { font-family: var(--font-mono); font-size: 0.65625rem; color: var(--text-faint); text-transform: capitalize; }

h1 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.625rem;
  letter-spacing: -0.2px;
  margin: 0 0 6px;
  text-wrap: balance;
}

.panel-head h1 {
  color: var(--accent);
  font-weight: 800;
}

.panel-head .panel-sub {
  color: var(--text);
}

.panel-sub {
  color: var(--text-muted);
  font-size: 0.875rem;
  margin: 0;
  max-width: 62ch;
}

.inline-link {
  color: var(--accent, #0A0A0A);
  font-weight: 600;
  text-decoration: none;
}

.inline-link:hover {
  text-decoration: underline;
}

/* ---------- console layout ---------- */

.console-grid {
  display: grid;
  grid-template-columns: minmax(0, 400px) 1fr;
  gap: 22px;
  align-items: start;
}

@media (max-width: 900px) {
  .console-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Mobile-only: surface the generated output higher up on Image and
   Character Sheet so the result is visible without scrolling past the
   whole controls card first. Desktop keeps controls-left/output-right via
   .console-grid's two-column layout above — this only reorders the two
   panels once they've already stacked into one column. No DOM change,
   no duplication: same two panels, just re-ordered with CSS `order`. */
@media (max-width: 900px) {
  #view-image .control-panel { order: 2; }
  #view-image .viewfinder-panel { order: 1; }
  #view-charsheet .control-panel { order: 2; }
  #view-charsheet .viewfinder-panel { order: 1; }
}

/* Image Generator: give it real desktop width like the video module (same
   full-width-column technique), but keep the console-grid's compact-left /
   dominant-right ratio intact — centering the whole [controls | output]
   row plus the History section beneath it within that wider column. */
/* Full-bleed brushed-metal + dot background: main itself becomes the
   background layer, spanning the true workspace edge-to-edge (sidebar to
   far right, top to bottom of the page) — independent of the centered
   1600px content column, which is re-applied directly on #view-image
   below so the actual content renders at exactly the same position/size
   as before. Same technique used for Projects. */
main:has(> #view-image.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  /* Dots folded directly into main's own background (not a ::before
     pseudo) so the pattern tiles across the full scrollable height
     instead of only the first viewport — see #view-explore's fix above. */
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-image.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}
/* Stretch both cards in the row to equal height — the output panel's
   bottom edge should land exactly on the controls panel's bottom edge,
   not stop short of it. */
#view-image .console-grid { align-items: stretch; }
#view-image .viewfinder-panel {
  min-height: 520px;
  height: 100%;
}
#view-image .viewfinder {
  position: relative;
  flex: 1;
  min-height: 420px;
}
/* Near-invisible dotted-grid canvas texture — tiny low-contrast dots,
   brighter toward the center and fading out toward the edges via a mask.
   Sits behind the actual generated image (z-index below), so it's only
   ever visible in unused canvas space once a result is showing. */
#view-image .viewfinder::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: radial-gradient(rgba(255, 255, 255, 0.09) 1px, transparent 1px);
  background-size: 20px 20px;
  -webkit-mask-image: radial-gradient(ellipse at center, black 0%, rgba(0, 0, 0, 0.55) 45%, transparent 85%);
  mask-image: radial-gradient(ellipse at center, black 0%, rgba(0, 0, 0, 0.55) 45%, transparent 85%);
  pointer-events: none;
  z-index: 0;
}
#view-image .viewfinder > * {
  position: relative;
  z-index: 1;
}

.panel {
  /* Lighter polished silver than the main brushed-metal page background,
     so panels/cards clearly sit as a raised control surface on top of it. */
  background: var(--panel-sheen), var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md), var(--inset-hairline);
  padding: 22px;
}

.type-switch {
  display: inline-flex;
  gap: 3px;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  padding: 3px;
  border-radius: 999px;
  margin-bottom: 20px;
}

.type-btn {
  background: transparent;
  border: none;
  color: var(--text-muted);
  padding: 6px 18px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}

.type-btn.active {
  background: var(--accent);
  color: #ffffff;
}

.field { margin-bottom: 16px; }
/* Shot Extractor's "Shot number" field, called out by name: the label and
   its stepper control must be explicitly grouped in one flex column, not
   left to rely on default block-stacking (which is what was there before —
   visually correct most of the time, but with no explicit grouping it can
   silently inherit width/offset from a sibling column if the surrounding
   grid layout ever changes). This makes the grouping structural instead of
   incidental. */
.shotex-shot-field {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}
.field:last-of-type { margin-bottom: 0; }

.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-bottom: 16px;
}

label {
  display: block;
  font-size: 0.71875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text-faint);
  margin-bottom: 7px;
}

.label-note {
  text-transform: none;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--text-faint);
  font-size: 0.6875rem;
}

select, textarea, input[type="text"], input[type="password"], input[type="email"] {
  width: 100%;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 10px 12px;
  font-size: 0.84375rem;
  font-family: var(--font-body);
  transition: border-color 0.12s ease;
}

textarea { resize: vertical; font-family: var(--font-body); line-height: 1.5; }

select:focus, textarea:focus, input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.file-drop {
  position: relative;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: 12px 14px;
  font-size: 0.8125rem;
  color: var(--text-muted);
  font-family: var(--font-mono);
}

.file-drop input[type="file"] {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}
/* Shows an actual thumbnail of the uploaded image inside the drop box
   instead of just the filename as plain text. */
.file-drop.has-image {
  min-height: 90px; background-size: cover; background-position: center;
  color: transparent; border-style: solid;
}
.file-drop.has-image span { display: none; }

.primary-btn {
  margin-top: 20px;
  width: 100%;
  background: var(--accent-gradient);
  color: #ffffff;
  border: none;
  padding: 13px;
  border-radius: var(--radius);
  font-family: var(--font-body);
  font-size: 0.90625rem;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 6px 20px -6px rgba(0, 0, 0, 0.55), var(--inset-hairline);
  transition: filter 0.12s ease, transform 0.1s ease, box-shadow 0.15s ease;
}

.primary-btn:hover { filter: brightness(1.08); transform: translateY(-1px); box-shadow: 0 10px 26px -6px rgba(0, 0, 0, 0.65), var(--inset-hairline); }
.primary-btn:active { transform: translateY(1px); box-shadow: 0 3px 10px -4px rgba(0, 0, 0, 0.5); }
.primary-btn:disabled { opacity: 0.5; cursor: not-allowed; filter: none; transform: none; box-shadow: none; }

.primary-btn:focus-visible, .secondary-btn:focus-visible, .type-btn:focus-visible,
.rail-btn:focus-visible, select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.secondary-btn {
  /* An explicit display, because this class is used on both <button> and <a>.
     A button defaults to inline-block and honours the vertical padding below;
     an anchor defaults to inline, where vertical padding does NOT grow the
     line box — it bleeds outside it and overlaps whatever is next. In the
     admin table that made the Get link result render taller than its own
     container and spill across the row beneath it. */
  display: inline-block;
  text-align: center;
  text-decoration: none;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  padding: 9px 16px;
  border-radius: var(--radius);
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
}
.secondary-btn:hover { background: var(--surface-3); }

.status-line {
  margin-top: 12px;
  font-family: var(--font-mono);
  font-size: 0.78125rem;
  color: var(--text-muted);
  min-height: 16px;
}
.status-line.error { color: var(--error); }

.job-error-card {
  margin-top: 6px; padding: 12px 14px; border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--error) 40%, var(--border));
  background: color-mix(in srgb, var(--error) 8%, var(--surface-2));
}
.job-error-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-bottom: 6px;
}
.job-error-title {
  font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.5px;
  text-transform: uppercase; color: var(--error);
}
.job-error-code {
  font-family: var(--font-mono); font-size: 0.65625rem; font-weight: 600; letter-spacing: 0.2px;
  color: var(--text-muted); background: var(--surface-3); border: 1px solid var(--border);
  border-radius: 999px; padding: 2px 9px; white-space: nowrap;
}
.job-error-message {
  font-family: var(--font-sans, inherit); font-size: 0.8125rem; line-height: 1.5; color: var(--text);
}
.job-error-request-id {
  margin-top: 8px; font-family: var(--font-mono); font-size: 0.65625rem; color: var(--text-faint);
}

/* ---------- character sheet ---------- */

/* Same real-desktop-width + centering treatment as Image/Video/Characters.
   Full-bleed brushed-metal + dot background: main itself becomes the
   background layer, spanning the true workspace edge-to-edge (sidebar to
   far right, top to bottom of the page) — independent of the centered
   1600px content column, which is re-applied directly on #view-charsheet
   below so the actual content renders at exactly the same position/size
   as before. */
main:has(> #view-charsheet.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  /* Dots folded directly into main's own background (not a ::before
     pseudo) so the pattern tiles across the full scrollable height
     instead of only the first viewport — see #view-explore's fix above. */
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-charsheet.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}

/* Controls and preview stretch to equal height, matching top and bottom
   edges, same as the Image page. */
#view-charsheet .console-grid { align-items: stretch; }
#view-charsheet .viewfinder-panel { height: 100%; min-height: 520px; }
#view-charsheet .viewfinder { position: relative; flex: 1; min-height: 420px; }

/* Same near-invisible dotted-grid canvas texture used on Image/Video/
   Characters — tiny low-contrast dots, brighter at center, fading toward
   the edges, sitting behind the generated sheet so it only shows in
   unused canvas space. */
#view-charsheet .viewfinder::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: radial-gradient(rgba(255, 255, 255, 0.09) 1px, transparent 1px);
  background-size: 20px 20px;
  -webkit-mask-image: radial-gradient(ellipse at center, black 0%, rgba(0, 0, 0, 0.55) 45%, transparent 85%);
  mask-image: radial-gradient(ellipse at center, black 0%, rgba(0, 0, 0, 0.55) 45%, transparent 85%);
  pointer-events: none;
  z-index: 0;
}
#view-charsheet .viewfinder > * { position: relative; z-index: 1; }

/* History as a horizontal carousel with wide cards sized for a 3-panel
   (face / front / back) sheet's aspect ratio, instead of the default
   square-ish portrait thumbnail grid — so each card actually shows the
   multi-panel output rather than cropping it down. */
#view-charsheet #charsheet-history-grid {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  gap: 16px;
  padding-bottom: 6px;
}
#view-charsheet #charsheet-history-grid .entity-card {
  flex: 0 0 360px;
}
#view-charsheet #charsheet-history-grid .entity-card .thumb,
#view-charsheet #charsheet-history-grid .entity-card img.thumb {
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* ---------- character workspace ---------- */

/* The old step-wizard nav (back arrows / crumbs / "continue" gate) stays in
   the DOM so app.js's existing listeners and text-setting code keep working
   unmodified, but it's never shown — the page below is one workspace, not a
   multi-step flow. */
.char-wizard-legacy-nav { display: none; }

.wizard-panel { padding: 24px; }
.wizard-step {}

/* Characters page: give it the same real desktop width as Image/Video,
   centered in the space to the right of the sidebar. Full-bleed brushed-
   metal + dot background: main itself becomes the background layer,
   spanning the true workspace edge-to-edge (sidebar to far right, top to
   bottom of the page) — independent of the centered 1600px content
   column, which is re-applied directly on #view-characters below so the
   actual content renders at exactly the same position/size as before.
   Same technique used for Projects/Image/Video. */
main:has(> #view-characters.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  /* Dots folded directly into main's own background (not a ::before
     pseudo) so the pattern tiles across the full scrollable height
     instead of only the first viewport — see #view-explore's fix above. */
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-characters.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}

.char-workspace { display: flex; flex-direction: column; gap: 22px; }

.char-top-row {
  display: grid;
  grid-template-columns: minmax(0, 420px) 1fr;
  gap: 22px;
  align-items: start;
}
@media (max-width: 980px) {
  .char-top-row { grid-template-columns: minmax(0, 1fr); }
}

/* Both top-row cards stretch to the same height — the shorter Style/Base
   panel uses the extra room for spacing/hierarchy, never by scaling the
   Male/Female thumbnails themselves. */
.char-top-row { align-items: stretch; }

.char-style-panel {
  display: flex;
  flex-direction: column;
  height: 100%;
}
.char-style-panel .style-grid { justify-content: flex-start; }
.char-style-panel .style-grid .style-card { width: calc(50% - 8px); }
/* Push the locked-style card toward the bottom of the panel once it's
   taller than its content needs, so the extra height reads as intentional
   breathing room between "choose a base" and "your locked style" rather
   than a stretched, empty gap. */
.char-style-panel .style-locked-card { margin-top: auto; margin-bottom: 0; }

/* Uses the same unified dark output-container treatment as Image/Video/
   Storyboard (the .viewfinder-panel base class, added alongside .panel in
   the HTML) — no separate light-silver framing, no gap between header and
   preview: the dark header and dark preview area are one continuous box,
   matching the other generation pages exactly. */
.char-preview-panel {
  display: flex;
  flex-direction: column;
  height: 100%;
}
.char-preview-viewfinder {
  flex: 1;
  position: relative;
  min-height: 400px;
}
/* Same near-invisible dotted-grid canvas texture used on the Image/Video
   output panels — tiny low-contrast dots, brighter at center, fading out
   toward the edges, sitting behind any generated character so it only
   shows in unused canvas space. */
.char-preview-viewfinder::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: radial-gradient(rgba(255, 255, 255, 0.09) 1px, transparent 1px);
  background-size: 20px 20px;
  -webkit-mask-image: radial-gradient(ellipse at center, black 0%, rgba(0, 0, 0, 0.55) 45%, transparent 85%);
  mask-image: radial-gradient(ellipse at center, black 0%, rgba(0, 0, 0, 0.55) 45%, transparent 85%);
  pointer-events: none;
  z-index: 0;
}
.char-preview-viewfinder > * { position: relative; z-index: 1; }
.char-preview-viewfinder .results-row:empty { display: none; }

.char-details-panel { display: flex; flex-direction: column; gap: 16px; }

/* Controls arranged as horizontal grouped columns instead of one long
   vertical form — every option in every group is shown at once (no "more
   details" accordion), wrapping naturally within its column so the panel
   stays scannable instead of one giant list. */
.attr-groups-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 14px 14px;
}
.attr-groups-row .attr-group {
  flex: 1 1 120px;
  min-width: 104px;
  margin-bottom: 0;
}
.attr-groups-row .attr-group > label {
  display: block;
  margin-bottom: 8px;
  /* Category headings (Ethnicity, Age Range, Hair Cut/Type, Hair Color,
     Facial Hair, Skin Tone, Eye Color) get the same soft-white heading
     color used elsewhere (Duration, Aspect Ratio, Resolution, Model, Batch
     Size, Reference Image) instead of the base <label> rule's muted
     --text-faint — color only, so size/weight/uppercase/letter-spacing/
     margin (and the section-level "2. Character details" heading above,
     which is unaffected) all stay exactly as they were. */
  color: var(--text-label);
}
.attr-groups-row .pill-row { flex-direction: row; flex-wrap: wrap; gap: 6px; }
.attr-groups-row .pill-toggle {
  padding: 6px 12px;
  font-size: 0.75rem;
}

/* Eye Color's optional Left/Right heterochromia expansion — appears only
   when "Heterochromia (two colors)" is selected. It grows the Eye Color
   column's own height only (align-items: flex-start above keeps sibling
   columns from stretching to match), never its width, so the other
   Character Details columns stay put. */
.hetero-expand {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 12px 18px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px dashed var(--border-soft);
}
.hetero-expand[hidden] { display: none; }
.hetero-eye-block { flex: 1 1 140px; min-width: 120px; }
.hetero-eye-label {
  display: block;
  margin-bottom: 6px;
  font-size: 0.6875rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-label);
}

.pill-toggle.has-swatch { display: inline-flex; align-items: center; gap: 7px; }
.pill-swatch {
  width: 12px; height: 12px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.25);
  flex: 0 0 auto;
}

.char-generate-row {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-top: 16px;
  border-top: 1px solid var(--border-soft);
}

/* History as a horizontal carousel — a single scrollable row instead of a
   wrapping grid, so it reads as "swipe/scroll sideways for more" rather
   than an ever-taller stack. */
#view-characters #characters-grid {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  gap: 16px;
  padding-bottom: 6px;
}
#view-characters #characters-grid .entity-card {
  flex: 0 0 190px;
}

.wizard-head {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  margin-bottom: 22px;
}
.wizard-head-3 { justify-content: space-between; flex-wrap: wrap; }
.wizard-head-left { display: flex; align-items: flex-start; gap: 14px; }

.wizard-back-btn {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-muted);
  width: 34px; height: 34px;
  border-radius: 999px;
  font-size: 0.9375rem;
  cursor: pointer;
  flex: 0 0 auto;
}
.wizard-back-btn:hover { color: var(--text); border-color: var(--accent); }

.wizard-crumb {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.8px;
  color: var(--text-faint);
  margin-bottom: 4px;
}
.wizard-title {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 700;
  margin: 0;
}

.style-grid {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 16px;
}
.style-grid .style-card { width: 220px; flex: 0 0 auto; }
@media (max-width: 480px) { .style-grid .style-card { width: 100%; max-width: 260px; } }

.style-card {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 0 0 12px;
  cursor: pointer;
  text-align: left;
  overflow: hidden;
  transition: border-color 0.12s ease, transform 0.06s ease;
  font-family: var(--font-body);
}
.style-card:hover { border-color: var(--text-faint); transform: translateY(-2px); }
.style-card.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.style-card.active:hover { border-color: var(--accent); }
.style-card-imgs {
  position: relative;
  width: 100%;
  padding-top: 100%;
  margin-bottom: 10px;
  overflow: hidden;
}
.style-card-imgs img {
  position: absolute;
  /* Explicit longhands instead of the `inset: 0` shorthand — Safari/WebKit
     has a documented history of inconsistently applying `inset` combined
     with object-fit/object-position on images (support for the shorthand
     itself shipped well after the individual longhands), which is a
     plausible explanation for why this crop measured correctly in every
     desktop check yet still rendered unevenly on a real iPhone. The four
     longhands below are universally supported and behave identically to
     inset:0 everywhere inset:0 already worked. */
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Clean Base — Male/Female example portraits only (see STYLE_PRESETS in
   app.js — data-style values "clean-base-male"/"clean-base-female"): the
   shared 1:1 cover crop above centers on the image's vertical midpoint,
   which cuts off the top of the head on these two specific source photos.
   Shifting the focal point upward (toward the top of the source image)
   keeps the same card size/crop shape but reveals the full head with a
   little headroom above it — every other style card keeps the default
   centered crop untouched. */
.style-card[data-style^="clean-base"] .style-card-imgs img {
  object-position: 50% 15%;
}
/* Follow-up #3 — the real fix. Follow-ups #1 and #2 above each only solved
   HALF of this: the male and female source photos don't just differ in
   crop offset, they differ in effective "zoom level" relative to their own
   frame. The male photo's hair spans edge-to-edge at 800px wide with the
   camera framed tighter on the head, while the female photo has visible
   background at the sides — so at ANY single object-position value, the
   male head is scaled up more within the square crop than the female
   head is, and the hairline-to-eye-line distance in the male card is
   simply bigger in pixels. That made #1 (object-position 0%) satisfy "hair
   touches the frame top like the female card" but leave the eyes sitting
   lower, and #2 (object-position 60%) satisfy "eyes align with the female
   card" but crop away the hairline entirely, reopening the original
   headroom complaint. A single crop offset cannot satisfy both at once
   when the head-to-frame scale itself doesn't match.
   The actual fix is in the source asset: clean-base-cover-male.jpg has
   been padded with 114px of its own background color split across both
   sides (800px photo -> 914px canvas, same height, background-matched so
   the seam is invisible), which — combined with the shared 1:1 cover crop
   above scaling to the new wider canvas — shrinks the head's effective
   size within the square frame by the same proportion the female photo
   already has. With that corrected, this card can use the SAME object-
   position as the shared rule above (0%, i.e. hair touching the frame top
   with no crop offset) and both the hairline alignment AND the eye-line
   now match the female card — verified against a pixel grid overlay.
   Female's own asset and its 15% offset are both untouched. */
.style-card[data-style="clean-base-male"] .style-card-imgs img {
  object-position: 50% 0%;
}
/* Follow-up #4 — after two rounds of object-fit/object-position tuning
   both measured correct in a real engine (Chrome) against the exact
   production image bytes, yet a real iPhone Safari session still showed
   an uneven crop between these two cards, the remaining explanation is a
   WebKit-specific object-fit/object-position reliability gap rather than
   a wrong value. Rather than tune numbers again, these two cards render
   via CSS background-image + background-size: cover instead — a
   technique with a much longer, more consistent cross-browser (including
   older WebKit) track record than object-fit on <img> has. The <img>
   tags stay in the DOM unchanged (markup/JS untouched, alt text intact)
   but are made invisible here so they can't double-render under the
   background layer; every other style card keeps using its <img> +
   object-fit exactly as before. */
.style-card[data-style^="clean-base"] .style-card-imgs {
  background-size: cover;
  background-repeat: no-repeat;
}
.style-card[data-style="clean-base-male"] .style-card-imgs {
  background-image: url('images/clean-base-cover-male.jpg?v=4');
  background-position: 50% 0%;
}
/* Follow-up #5 reverted — an 8%-each-side in-crop of this asset was tried
   here to close a perceived headroom gap vs. the male card, but the
   account owner supplied the original, uncropped photo back as the
   correct/intended image and asked for it restored exactly as given, with
   no further crop tuning. clean-base-cover.jpg is that original 800px-wide
   photo again; only object-position/background-position (50% 15%, set
   below) adjusts the crop, never the source asset itself. */
.style-card[data-style="clean-base-female"] .style-card-imgs {
  background-image: url('images/clean-base-cover.jpg?v=4');
  background-position: 50% 15%;
}
.style-card[data-style^="clean-base"] .style-card-imgs img {
  opacity: 0;
}
/* Mobile previously carried two device-specific overrides here (a
   scale()+translateY() nudge on Male, a lowered object-position on Female)
   added to chase alignment/crop complaints independently on each side.
   Removed per correction: desktop's crop (this file, just above) is already
   the correct, verified framing for both source photos, and mobile is
   meant to render the exact same object-fit/object-position logic on the
   exact same assets, just inside a smaller responsive card — not carry its
   own separately-tuned crop that can drift out of sync with desktop again.
   No mobile-only image rule intentionally remains here. */
.style-card-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.84375rem;
  padding: 0 10px;
  margin-bottom: 3px;
}
.style-card-desc {
  font-style: italic;
  font-size: 0.71875rem;
  color: var(--text-faint);
  padding: 0 10px;
}

/* Mobile-only card alignment fix (Characters page Clean Base cards, but
   applied to every .style-card so any two titles of different lengths stay
   consistent). The image area above is always a fixed square regardless of
   what follows it, so it never actually shifts on its own — but "Clean
   Base — Male" fits on one line while "Clean Base — Female" wraps to two,
   making the Female card's title block taller. .style-grid is a flex
   container with the default align-items: stretch, so cards in the same
   row get stretched to match the tallest one, and the shorter (Male) card
   absorbs that extra height whenever its own content doesn't already fill
   it — visually reading as inconsistent framing between the two cards even
   though each image itself is an untouched, correctly-cropped square.
   Reserving 2 lines of height for both the title and description on mobile
   makes every card's non-image content the same height up front, so no
   stretching is needed and both cards line up edge-to-edge. Desktop is
   unaffected; no image, crop, or object-position value changes here. */
@media (max-width: 640px) {
  .style-card-name {
    min-height: 2.6em;
    display: flex;
    align-items: flex-start;
  }
  .style-card-desc {
    min-height: 2.6em;
  }
}

.style-locked-card {
  display: flex;
  align-items: center;
  gap: 14px;
  background: var(--surface-2);
  border: 1px solid var(--accent-soft-2);
  border-radius: var(--radius-lg);
  padding: 12px 16px;
  margin-bottom: 22px;
}
.style-locked-card img {
  width: 56px; height: 56px;
  border-radius: var(--radius);
  object-fit: cover;
  flex: 0 0 auto;
}
.style-locked-label {
  font-family: var(--font-mono);
  font-size: 0.65625rem;
  font-weight: 700;
  letter-spacing: 0.8px;
  color: var(--accent);
  margin-bottom: 2px;
}
.style-locked-name { font-family: var(--font-display); font-weight: 700; font-size: 0.9375rem; }
.style-locked-desc { font-style: italic; font-size: 0.75rem; color: var(--text-faint); }

.attr-group { margin-bottom: 18px; }
.pill-row { display: flex; flex-wrap: wrap; gap: 8px; }

.pill-toggle {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-muted);
  padding: 8px 15px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: 0.78125rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.pill-toggle:hover { border-color: var(--accent); color: var(--text); }
.pill-toggle.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #ffffff;
}

/* Camera pills — same pill shape, plus a small muted secondary label
   underneath the camera name (e.g. "Mirrorless") for quick scanning.
   Cosmetic only; the value sent to generation is unchanged. */
.pill-row-labeled .pill-toggle {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 7px 15px 8px;
}
.pill-toggle-main { font-size: 0.78125rem; font-weight: 600; line-height: 1.2; }
.pill-toggle-sub {
  font-size: 0.59375rem;
  font-weight: 600;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--text-faint);
  line-height: 1.2;
}
.pill-toggle.active .pill-toggle-sub { color: rgba(255, 255, 255, 0.7); }

/* Lens pills — plain focal-length labels in a contained horizontal-scroll
   track (reusing the visual-selector arrow/scroll mechanism), full lens
   metadata surfaces only via the native title-attribute tooltip. */
.lens-track { gap: 8px; padding: 2px 2px 4px; }
.lens-pill {
  flex: 0 0 auto;
  padding: 8px 16px;
}

.wizard-footer {
  display: flex;
  justify-content: flex-end;
  margin-top: 24px;
}
.pill-btn { width: auto; margin-top: 0; border-radius: 999px; padding: 12px 22px; }

.summary-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.summary-chip {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 0.65625rem;
  font-weight: 600;
  letter-spacing: 0.4px;
  padding: 7px 10px;
  border-radius: var(--radius);
  /* Read-only summary labels, not buttons — no pointer cursor, no hover
     state, nothing to click. */
  cursor: default;
  user-select: none;
  display: flex; gap: 5px; align-items: center;
}
.summary-chip span { color: var(--text); font-weight: 700; }

.char-summary-block {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 14px;
  margin-top: 18px;
  margin-bottom: 4px;
}
.char-summary-block[hidden] { display: none; }
.char-summary-label {
  font-family: var(--font-mono);
  font-size: 0.65625rem;
  font-weight: 700;
  letter-spacing: 0.8px;
  color: var(--accent);
  margin-bottom: 4px;
}
.char-summary-line {
  font-family: var(--font-body);
  font-size: 0.8125rem;
  color: var(--text);
}

.generate-row {
  display: flex;
  gap: 10px;
  margin-top: 6px;
}
.generate-row input[type="text"] { flex: 1; }
.generate-row .primary-btn { flex: 0 0 auto; white-space: nowrap; }

/* Mobile: the "Add optional details" input was being squeezed into one tiny
   box alongside the Enhance and Generate buttons. Give it its own full-width
   line, with Enhance + Generate sharing the line below it (Generate stays
   visually directly beneath the input, above nothing else). */
@media (max-width: 860px) {
  .generate-row { flex-wrap: wrap; }
  .generate-row input[type="text"] { flex: 1 1 100%; }
}

.generate-helper {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  font-size: 0.71875rem;
  color: var(--text-faint);
  margin-top: 8px;
}
.generate-cost { font-family: var(--font-mono); }

.results-toolbar[hidden] { display: none; }
.results-toolbar {
  display: flex;
  gap: 8px;
  margin-top: 26px;
  margin-bottom: 10px;
}
.results-toolbar .icon-btn {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-muted);
  width: 30px; height: 30px;
  border-radius: var(--radius);
  cursor: pointer;
}
.results-toolbar .icon-btn:hover { color: var(--text); border-color: var(--accent); }

.results-row {
  display: flex;
  gap: 14px;
  overflow-x: auto;
  padding-bottom: 6px;
}
/* Just the generated image itself, at its real aspect ratio — no frame,
   no background fill, no caption. Height-capped to fit the preview strip;
   width follows naturally from the image's own proportions. */
.result-card {
  flex: 0 0 auto;
  max-height: 420px;
  width: auto;
  max-width: 100%;
  display: block;
  border-radius: var(--radius-lg);
  cursor: zoom-in;
}
.status-line.success { color: var(--success); }

/* ---------- viewfinder (output preview) ---------- */

.viewfinder-panel {
  padding: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-height: 460px;
  /* Always a dark camera-style display, regardless of the silver theme
     applied to every other panel via the generic .panel rule — see the
     --vf-* tokens in :root. */
  background: var(--panel-sheen), var(--vf-bg);
  border-color: var(--vf-border);
}

.viewfinder-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px;
  border-bottom: 1px solid var(--vf-border);
  /* Explicit fill + matching corner radii so the header's dark background
     fully covers the top-left curve exactly like the top-right curve —
     without this, the panel's own rounded-corner clip can leave a hairline
     sliver of the page background showing through the top-left corner. */
  background: var(--vf-bg);
  border-top-left-radius: var(--radius-lg);
  border-top-right-radius: var(--radius-lg);
}

.viewfinder-title {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  color: var(--vf-text-label);
}

.viewfinder-tag {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--vf-surface-2);
  color: var(--vf-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.viewfinder-tag.running { background: var(--accent-soft); color: var(--vf-text); }
.viewfinder-tag.completed { background: var(--success-soft); color: var(--success); }
.viewfinder-tag.failed { background: var(--error-soft); color: var(--error); }
.viewfinder-tag.canceled { background: var(--error-soft); color: var(--error); }

.viewfinder-head .icon-btn {
  background: var(--vf-surface-2);
  border-color: var(--vf-border);
  color: var(--vf-text-muted);
}
.viewfinder-head .icon-btn:hover:not(:disabled) {
  background: var(--vf-surface-3);
  color: var(--vf-text);
  border-color: var(--vf-text-muted);
}

.viewfinder {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(180deg, var(--vf-surface-2), var(--vf-bg));
  border-radius: var(--radius-lg);
  padding: 24px;
  /* The frame below sizes itself from the result's real aspect ratio, which
     can be much taller (or wider) than this box. Without clipping here, a
     tall result would visually spill out past this box's rounded corners
     instead of shrinking to fit inside it. */
  overflow: hidden;
}

.viewfinder-empty {
  text-align: center;
  color: var(--vf-text-faint);
}

.viewfinder-empty-glyph {
  display: block;
  font-size: 1.75rem;
  color: var(--vf-border);
  margin-bottom: 10px;
}

.viewfinder-empty p { margin: 0; font-size: 0.8125rem; }

.viewfinder-frame {
  /* Sized relative to the actual .viewfinder parent box (not the viewport)
     so the frame always shrinks to fit inside it, no matter the result's
     real aspect ratio. width/height:auto + max-width/max-height:100% is the
     standard "contain within parent" pattern: the browser picks whichever
     dimension (width or height) is the limiting one for this aspect-ratio
     and scales the other to match, so the box never exceeds its parent in
     either direction. */
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  aspect-ratio: var(--frame-ratio, 16 / 9);
  border-radius: 6px;
  overflow: hidden;
  background: var(--vf-surface-2);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
}
.viewfinder-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Video uses "contain" (not "cover") so a portrait/mobile-shot clip is never
   cropped to fill a landscape box — the frame reshapes to the video's real
   aspect ratio at runtime (see renderResultFrame's onloadedmetadata), so
   this is mostly a safety net for the brief moment before that fires. */
.viewfinder-frame video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
  display: block;
}
/* Batch outputs (Image/Video Generator, Batch Size > 1) — a responsive grid
   that adapts to how many results there are instead of a fixed column count:
   2 results sit in two even columns, 3 balance across the available width,
   4 settle into a clean 2x2 on desktop, and anything narrower than that
   (mobile, or a narrow panel) collapses to one column so tiles never get
   squeezed thinner than they can display cleanly. auto-fit + minmax lets the
   browser pick the natural column count per width rather than a single
   hardcoded layout; the data-count overrides below pin the two shapes the
   spec calls out explicitly (even pairs, clean 2x2) so they're consistent
   regardless of exact panel width. */
.viewfinder-multi {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 16px;
  width: 100%;
  align-items: start;
}
.viewfinder-multi[data-count="2"],
.viewfinder-multi[data-count="4"] {
  grid-template-columns: repeat(2, 1fr);
}
.viewfinder-multi .viewfinder-frame { max-width: none; width: 100%; }

/* A failed job inside a batch gets its own clearly-marked tile in its slot —
   same box shape/aspect-ratio as a successful result — rather than being
   silently dropped or breaking the grid, so the successful outputs around it
   stay exactly where they are. */
.viewfinder-frame-failed {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--error-soft);
  border: 1px dashed var(--error);
}
.viewfinder-frame-failed-inner {
  text-align: center;
  padding: 16px;
  color: var(--error);
}
.viewfinder-frame-failed-icon { display: block; font-size: 1.25rem; margin-bottom: 6px; }
.viewfinder-frame-failed-msg { display: block; font-size: 0.71875rem; line-height: 1.4; word-break: break-word; }

/* The output area normally matches a fixed single-frame height (image page:
   matches the left control panel's height; video page: locked to a 16:9
   box). A batch grid needs to grow with its content instead — natural height
   with a generous cap, scrolling internally past that, so several rows of
   results can never spill outside the panel or blow out the page layout. */
.viewfinder.multi-result {
  display: block;
  align-items: initial;
  justify-content: initial;
}
#view-image .viewfinder.multi-result {
  height: auto;
  max-height: 80vh;
  overflow-y: auto;
}
.video-console-grid .viewfinder-panel .viewfinder.multi-result {
  aspect-ratio: auto;
  max-height: 900px;
  overflow-y: auto;
}

@media (max-width: 640px) {
  .viewfinder-multi { grid-template-columns: 1fr !important; }
}

.viewfinder-head-actions { display: flex; align-items: center; gap: 8px; }
.viewfinder-head-end { justify-content: flex-end; }

/* Storyboard grid + Shot Extractor reference/output — these images (a 2×2
   grid meant to be inspected panel-by-panel, or a full-detail extracted
   frame) must never be cropped or squeezed into a small preview box the
   way a normal single-shot result is. The panel gets extra room and the
   image is sized purely by its own real aspect ratio (max-width/max-height
   with width/height:auto — no object-fit:cover, no forced aspect-ratio box)
   so it can never be cropped, stretched, or distorted, and stays centered
   with clear margins around it. The viewfinder panel itself is also taller
   than the default (560px vs. 460px) so it reads as a primary working
   surface, not an attachment preview, and can still grow if the image is
   tall. */
.storyboard-viewfinder {
  min-height: 560px;
  padding: 28px;
}
.storyboard-frame {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
}
.storyboard-frame img {
  max-width: 100%;
  max-height: 78vh;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
  border-radius: 6px;
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
  background: var(--vf-surface-2);
}
.viewfinder-panel-footer {
  padding: 14px 18px;
  border-top: 1px solid var(--vf-border);
  display: flex;
  justify-content: flex-end;
}
@media (max-width: 640px) {
  .storyboard-viewfinder { min-height: 380px; padding: 16px; }
  .storyboard-frame img { max-height: 60vh; }
}

/* Full-bleed brushed-metal + dot background: main itself becomes the
   background layer, spanning the true workspace edge-to-edge (sidebar to
   far right, top to bottom of the page) — independent of the content
   column, which is re-applied directly on #view-storyboard below at its
   original size/position (this page never had a wide-column override —
   it uses main's own default 1180px/flush-left layout, reproduced here
   unchanged) so the actual content renders exactly as before. Same
   technique used for Projects/Image/Video/Characters/Character Sheet/
   Scene. */
main:has(> #view-storyboard.active) {
  max-width: none;
  padding: 0;
  position: relative;
  /* Dots folded directly into main's own background (not a ::before
     pseudo) so the pattern tiles across the full scrollable height
     instead of only the first viewport — see #view-explore's fix above. */
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-storyboard.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1180px;
  margin-left: auto;
  margin-right: auto;
}

/* Storyboard / Shot Extractor page layout — output panels are full-width
   blocks stacked vertically (top: output, middle: controls, bottom:
   history) rather than a console-grid split with the controls beside the
   output. That's what keeps the storyboard the visual priority and avoids
   a tall narrow settings column next to a huge empty right-hand area. */
.storyboard-output-panel,
.storyboard-controls-panel {
  margin-bottom: 22px;
}
.storyboard-viewfinder-wide .storyboard-frame {
  max-width: 1400px;
  margin: 0 auto;
}
.ratio-badge {
  pointer-events: none;
  cursor: default;
}

/* Controls arranged left-to-right instead of one tall narrow column —
   collapses to 2 then 1 column as the viewport narrows. */
.storyboard-controls-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
  margin-bottom: 18px;
}
.storyboard-control-col {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}
@media (max-width: 960px) {
  .storyboard-controls-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .storyboard-controls-grid { grid-template-columns: 1fr; }
}

/* Shot Extractor centering — the page otherwise sits flush left inside
   main's grid column on wide viewports (main is capped at 1180px but has
   no auto margins), leaving a large unused strip on the right. Centering
   main itself only while this view is active balances the margins without
   touching any other page's layout.
   Full-bleed brushed-metal + dot background: main itself becomes the
   background layer, spanning the true workspace edge-to-edge (sidebar to
   far right, top to bottom of the page) — independent of the centered
   1560px content column, which is re-applied directly on
   #view-shot-extractor below so the actual content renders at exactly
   the same position/size as before. This page previously had no dot
   texture at all. */
main:has(> #view-shot-extractor.active) {
  margin: 0;
  max-width: none;
  padding: 0;
  position: relative;
  /* Dots folded directly into main's own background (not a ::before
     pseudo) so the pattern tiles across the full scrollable height
     instead of only the first viewport — see #view-explore's fix above. */
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-shot-extractor.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1560px;
  margin-left: 0;
  margin-right: auto;
}

/* Shot Extractor top row: a compact storyboard-reference panel (just big
   enough to identify the source storyboard / shot numbers) sits beside the
   larger extracted-shot output panel (the primary result to inspect) on
   desktop, stacking vertically on narrow viewports. */
.shotex-top-row {
  display: grid;
  /* Locked 2:1 fr ratio with NO fixed-px floor on either side — a
     minmax(240px, 1fr) floor here previously overrode the ratio and
     collapsed it to roughly 1:1 on any container narrower than ~1200px
     (i.e. almost always, once the sidebar is open), which is why the
     Storyboard Reference panel kept rendering far larger than 2:1 despite
     the fr values looking correct. minmax(0, Nfr) is what actually keeps
     the ratio true at every width; the 3fr:1fr media query below caps how
     far it can widen on very large screens without introducing a floor
     that fights the ratio again. */
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: 20px;
  margin-bottom: 22px;
  align-items: stretch;
}
@media (min-width: 1400px) {
  /* On very wide screens, cap the ratio at 3:1 instead of letting 2fr:1fr
     keep scaling the reference panel indefinitely. */
  .shotex-top-row { grid-template-columns: minmax(0, 3fr) minmax(0, 1fr); }
}
.shotex-ref-panel,
.shotex-extracted-panel {
  margin-bottom: 0;
  height: 100%;
}
.shotex-ref-viewfinder {
  min-height: 280px;
  padding: 16px;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  /* Solid near-black — not the lighter graphite-to-black gradient the base
     .viewfinder class uses — so unused space around a contained image reads
     as black, not grey, no matter how much of the box the image fills. */
  background: var(--vf-bg);
}
.shotex-ref-viewfinder .storyboard-frame {
  height: 100%;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.shotex-ref-viewfinder .storyboard-frame img {
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  object-position: center;
  display: block;
}
@media (max-width: 900px) {
  .shotex-top-row { grid-template-columns: 1fr; }
}

/* Compact numeric stepper (Shot Number) — replaces one segmented button
   per shot so the control stays a fixed size regardless of how many shots
   the current storyboard has (up to 12, structurally) rather than needing
   a button added/removed per layout. */
.value-stepper {
  display: inline-flex;
  align-items: center;
  gap: 16px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 10px;
  width: fit-content;
}
.value-stepper-btn {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--surface-3);
  border: 1px solid var(--border-soft);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 1rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.value-stepper-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.value-stepper-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.value-stepper-value {
  font-family: var(--font-mono);
  font-size: 0.875rem;
  font-weight: 700;
  color: var(--text);
  min-width: 64px;
  text-align: center;
}
.icon-btn {
  width: 26px; height: 26px; border-radius: 6px;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted);
  font-size: 0.8125rem; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.icon-btn:hover:not(:disabled) { background: var(--surface-3); color: var(--text); border-color: var(--accent); }
.icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }

.entity-card-actions {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 0 14px 12px;
}
.entity-card-actions .icon-btn { background: var(--surface-2); }

/* "Send to Character Sheet / Scene" handoff menu — reused by history/library
   cards (.entity-card-actions) and the just-generated result view
   (.viewfinder-result-actions). A compact button + tiny absolute-positioned
   popover, not a redesign of either surface it appears in. */
.sendto-wrap { position: relative; }
.sendto-wrap > .icon-btn { width: auto; padding: 0 8px; white-space: nowrap; font-size: 0.75rem; }
.sendto-menu {
  position: absolute; bottom: calc(100% + 4px); left: 0; z-index: 20;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
  display: flex; flex-direction: column; min-width: 140px; overflow: hidden;
}
.sendto-menu button {
  border: none; background: none; text-align: left; padding: 8px 12px;
  font-size: 0.8125rem; color: var(--text); cursor: pointer;
}
.sendto-menu button:hover { background: var(--surface-3); }
.viewfinder-result-actions { display: flex; justify-content: flex-end; padding: 8px 2px 0; }

/* ---------- workflow / node canvas ---------- */

#view-workflow {
  margin: -36px -44px -60px;
  max-width: none;
  width: auto;
}

#view-workflow.active {
  /* The canvas below must cover the ENTIRE black rectangle from the
     instructions text down to the bottom of the screen, with no dead
     zones. Relying on JS to measure remaining viewport space and set an
     inline pixel height (the old resizeNodeCanvas() approach) is a race:
     it runs once on view-switch, but anything that changes layout AFTER
     that point (web fonts finishing their swap and reflowing the "Workflow"
     title, images loading, a slow paint) shifts the canvas's top edge
     without ever re-measuring — leaving a flat black strip, with no dots,
     in the gap between the canvas's stale height and the real viewport
     bottom. Making this a flex column with the canvas set to flex:1
     guarantees the canvas box always exactly fills whatever space is left
     below the header, continuously, with no JS measurement involved. */
  height: 100%;
  /* FILL THE PARENT, don't self-size to the viewport. This element lives inside
     main, which the .app grid already sizes to the (dvh-based) viewport — so a
     plain height:100% makes the workflow column exactly as tall as the space
     it's given, no taller. The old min-height:100vh/100dvh forced it to a FULL
     viewport height regardless of the appbar/main box around it, so on iOS
     Safari landscape (where 100vh is the tall layout viewport) the column
     overran the visible area and pushed the centered quick-add row and the
     bottom tool dock off the bottom edge. main is a definite height here (the
     grid stretches it), so height:100% resolves cleanly — verified it fills
     exactly with the dock landing on-screen. min-height:0 removes the overflow
     source entirely; no viewport unit is involved, so there's nothing to
     mismatch the visible height on any device. */
  min-height: 0;
  display: flex;
  flex-direction: column;
}

#view-workflow .panel-head-row { padding: 18px 24px 0; flex: 0 0 auto; }

/* Wraps #node-canvas (the pannable/scrollable viewport) and
   #node-viewport-controls (the fixed overlay chrome: tool dock, zoom
   control, minimap, count pill, FAB) as siblings. #node-canvas owns the
   flex sizing that used to live directly on .node-canvas; the overlay
   is absolutely positioned over the same box so its lower-right/left-edge
   anchors never move when the canvas beneath it scrolls or zooms. */
.node-canvas-wrap {
  position: relative;
  flex: 1 1 auto;
  min-height: 420px;
  width: 100%;
}
@media (max-width: 860px) {
  .node-canvas-wrap { min-height: 380px; }
}

.node-viewport-controls {
  position: absolute;
  inset: 0;
  z-index: 30;
  pointer-events: none;
}
.node-viewport-controls > * { pointer-events: auto; }

.node-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  background:
    radial-gradient(circle, var(--dot-color, var(--black-dots)) 1.3px, transparent 1.3px) 0 0 / 22px 22px,
    var(--bg);
  background-repeat: repeat, no-repeat;
  /* background-attachment defaults to "scroll", which pins the background
     to the element's own box rather than to its scrolled content. Since
     this canvas is itself a scroll container (overflow:auto on mobile,
     and effectively whenever node content exceeds the box), that left the
     dot pattern stuck at the top on scroll — everything scrolled into view
     below the original box height rendered as solid background with no
     dots. "local" ties the background to the content instead, so the dot
     grid scrolls (and keeps tiling) along with the nodes. */
  background-attachment: local, local;
  border: none;
  border-radius: 0;
  box-shadow: none;
  /* Was overflow:hidden — that silently made wfCanvas.scrollLeft/scrollTop
     (which is how the Hand tool and node-drag edge-autoscroll pan the
     canvas, see app.js) a no-op on desktop: an element can't scroll at all
     while overflow:hidden, regardless of how much content the layer holds.
     With zero nodes there was also no content wide/tall enough to create a
     scrollable range even where overflow did allow it (see
     .node-canvas-layer's own comment) — so panning appeared to only "work"
     when a drag happened to grab a node (which repositions via left/top,
     independent of scroll) rather than the canvas itself ever moving.
     overflow:auto restores real scrolling; the scrollbar itself is hidden
     immediately below so nothing about the page's visual design changes. */
  overflow: auto;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none;
  user-select: none;
}
.node-canvas::-webkit-scrollbar { display: none; width: 0; height: 0; } /* WebKit/Chromium */

@media (max-width: 900px) {
  #view-workflow { margin: -20px -14px -40px; }
}

@media (max-width: 860px) {
  .node-canvas {
    min-height: 380px;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
  }
  .node { width: 240px; }
  /* Image/Video nodes previously kept their wider desktop widths
     (300px/320px) here, so on the smallest phone canvases they couldn't be
     fully seen without an extra horizontal scroll inside the (already
     scrollable/contained) node canvas. Bring them in line with the plain
     .node mobile width for consistency — this never affects the outer page,
     only the pannable canvas's own content. */
  .node-type-image { width: 240px; }
  .node-type-video { width: 240px; }
}

.field-row-inline { display: flex; align-items: center; gap: 10px; }
.field-row-inline label:first-child { margin: 0; }
.toggle-switch {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  position: relative;
}
.toggle-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle-slider {
  width: 36px; height: 20px; border-radius: 999px;
  background: var(--surface-3); border: 1px solid var(--border-soft);
  position: relative; flex-shrink: 0; transition: background 0.15s;
}
.toggle-slider::before {
  content: ""; position: absolute; top: 1px; left: 1px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--text-faint); transition: transform 0.15s, background 0.15s;
}
.toggle-switch input:checked + .toggle-slider { background: var(--accent); border-color: var(--accent); }
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(16px); background: #fff; }
.toggle-switch-text { font-size: 0.78125rem; color: var(--text-muted); }

.node-canvas-hint {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  font-size: 0.75rem; color: var(--text-faint); opacity: 0.7; background: rgba(0, 0, 0, 0.04);
  border: 1px solid rgba(0, 0, 0, 0.1); padding: 6px 13px; border-radius: 999px;
  pointer-events: none; white-space: nowrap; letter-spacing: 0.1px;
}
.node-canvas.has-nodes .node-canvas-hint { display: none; }

.node-links {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
  z-index: 1;
}

.node-palette {
  position: absolute;
  width: 230px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
  padding: 6px;
  z-index: 20;
}
.node-palette-item {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: var(--radius); cursor: pointer;
}
.node-palette-item:hover { background: var(--surface-3); }
.np-icon { width: 24px; text-align: center; color: var(--accent); font-size: 0.875rem; flex-shrink: 0; }
.np-label { font-size: 0.8125rem; font-weight: 600; color: var(--text); }
.np-sub { font-size: 0.6875rem; color: var(--text-faint); }

.node {
  position: absolute;
  width: 280px;
  z-index: 2;
}
.node-type-image { width: 360px; }
/* Video nodes get a few extra px (not shared with Image) so the new
   Duration control fits alongside model/mode/aspect/resolution without
   crowding the toolbar. */
.node-type-video { width: 404px; }
.node.dragging { z-index: 10; }
.node.selected .node-box { border-color: var(--accent); }

/* --- generator nodes (Image/Video): floating toolbar + one unified box --- */

/* Both the pill toolbar (model/mode/aspect/resolution/duration/sound) AND
   the compact label row (icon/type/model/cost/delete) are hidden by
   default — an inactive node shows nothing above its own box, full stop.
   Clicking the node is what reveals them, in place, right above the box;
   clicking a different node (or empty canvas) hides them again via the
   same .selected toggle used everywhere else on the node. Nothing here
   overflows past the node's own width, so even once revealed it reads as
   part of the node rather than a separate floating panel. */
.node-toolbar {
  display: none;
  align-items: center; gap: 8px; flex-wrap: nowrap;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  height: 42px;
  padding: 0 8px 0 14px;
  margin-bottom: 10px;
  cursor: grab;
  touch-action: none;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3), 0 0 0 1px var(--accent);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
}
.node-toolbar::-webkit-scrollbar { display: none; }
.node-toolbar:active { cursor: grabbing; }
.node.selected .node-toolbar { display: flex; }

.tb-controls { display: flex; align-items: center; gap: 7px; flex-wrap: nowrap; flex-shrink: 0; }

/* --- label row: icon + type + model name + cost badge + delete — hidden
   until the node is selected, same as the toolbar above it. --- */
.node-label-row {
  display: none;
  align-items: center; gap: 6px;
  padding: 0 2px 8px;
  font-size: 0.6875rem; color: var(--text-muted);
}
.node.selected .node-label-row { display: flex; }
.nlr-icon { color: var(--node-color, var(--accent)); font-size: 0.75rem; flex-shrink: 0; }
.nlr-type { font-weight: 700; color: var(--text); text-transform: none; }
.nlr-model {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text-faint); font-family: var(--font-mono); font-size: 0.65625rem;
}
/* Read-only, auto-inferred generation route (see inferWorkflowNodeMode in
   app.js) — deliberately styled as plain informational text, never as a
   button/select, since it replaces what used to be a manual dropdown. */
.nlr-inferred-mode {
  font-size: 0.625rem; font-weight: 700; letter-spacing: 0.02em;
  color: var(--text-faint); white-space: nowrap; flex-shrink: 0;
}
.nlr-inferred-mode-warning { color: var(--danger); }
/* ---------- Workflow Image node: per-node Moodboard control ----------
   Lives in the node's TOP toolbar (.tb-controls), alongside model/aspect —
   never buried in the prompt body — and is measured by the same
   measureWfToolbarWidth()/getWorkflowNodeMinWidth() toolbar-fit sizing every
   other toolbar control already uses, so the node widens to fit it rather
   than anything here ever clipping or cramming. Silver/graphite palette to
   match the rest of the toolbar — no neon, no blue, no orange. */
.wf-moodboard-btn {
  display: flex; align-items: center; gap: 5px;
  height: 30px; padding: 0 10px; border-radius: 999px;
  background: var(--surface-3); border: 1px solid var(--border-soft);
  color: var(--text-muted); font-size: 0.6875rem; font-weight: 700;
  font-family: var(--font-mono); letter-spacing: 0.01em;
  cursor: pointer; white-space: nowrap; flex-shrink: 0;
  max-width: 160px;
}
.wf-moodboard-btn .wf-mb-label { overflow: hidden; text-overflow: ellipsis; max-width: 128px; }
/* "MB" static prefix so the dynamic label can show just the value (NONE /
   the Moodboard's own name) without repeating the word "Moodboard" inside
   it — same de-duplication as the shared MOODBOARD [ ... ] selector used on
   every generation page (see renderMoodboardSelector). */
.wf-moodboard-btn .wf-mb-prefix { opacity: 0.65; }
.wf-moodboard-btn:hover { border-color: var(--accent); color: var(--text); }
/* Active (a READY Moodboard is applying) — same "black selected state on a
   light pill" language as the Select/Hand tool dock's own .active state. */
.wf-moodboard-btn.active-state { background: #fff; border-color: #fff; color: #2a2a2e; }
.wf-moodboard-btn.unavailable { color: var(--danger); border-color: var(--danger); }
/* Lives as a direct child of #node-viewport-controls (see index.html) — the
   fixed, never-pans/never-scales overlay the zoom control and tool dock also
   use — rather than nested inside the node/toolbar DOM. An earlier version
   nested it inside .node-toolbar, whose own overflow-y:hidden (an unrelated
   horizontal-scroll-fallback rule) silently clipped it the instant it
   opened; the click handler was firing correctly the whole time, it just had
   nowhere visible to render. left/top are computed and set inline by
   positionWfMoodboardMenu() in app.js against whichever node's button was
   clicked (flips above / nudges left near an edge), so only the fixed
   visual chrome lives here in CSS. */
.wf-moodboard-menu {
  position: absolute; z-index: 50;
  min-width: 180px; max-width: 240px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 10px; padding: 6px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
  display: flex; flex-direction: column; gap: 2px;
  cursor: default;
}
.wf-moodboard-menu.hidden { display: none; }
.wf-moodboard-menu button {
  display: flex; align-items: center; gap: 8px;
  background: transparent; border: none; color: var(--text);
  font-size: 0.75rem; text-align: left; padding: 7px 8px; border-radius: 6px; cursor: pointer;
  width: 100%;
}
.wf-moodboard-menu button:hover:not(:disabled) { background: var(--surface-3); }
.wf-moodboard-menu button.wf-mb-current { font-weight: 700; }
.wf-moodboard-menu button:disabled { color: var(--text-faint); cursor: default; }
.wf-mb-check { width: 14px; flex-shrink: 0; color: var(--text); text-align: center; }
.wf-mb-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nlr-cost {
  font-size: 0.6875rem; font-weight: 800; color: #0a0a0a;
  background: #fff; border-radius: 6px; padding: 2px 7px; flex-shrink: 0;
}
.node-label-row .node-close { margin-left: auto; }
.tb-icon { color: var(--node-color, var(--accent)); font-size: 0.8125rem; flex-shrink: 0; }

/* Every control below is given real breathing room — larger type, wider
   pills, more internal padding and inter-control gap — so labels stop
   getting cut off and dropdown values are actually legible. Same presets,
   same options, same order, same icons; only the sizing changed. The
   toolbar's own extra width (above) plus its horizontal-scroll fallback
   absorb whatever this no longer fits at the node's original width. */
.tb-select {
  font-size: 0.75rem; padding: 6px 10px; border-radius: 999px;
  background: var(--surface-3); border: 1px solid var(--border-soft); color: var(--text);
  max-width: 92px; min-width: 0; flex-shrink: 1;
}
.tb-select.hidden { display: none; }
/* Video toolbar has one extra control (Duration) versus Image's — still
   given real width, just a bit less than Image's since there's one more
   pill sharing the row; the toolbar's own overflow-x handles the rest. */
.node-type-video .tb-select { max-width: 76px; }
.node-type-video .tb-name { width: 68px; }
.wf-duration { max-width: 60px; }
.tb-icon-btn {
  height: 30px; flex-shrink: 0; white-space: nowrap;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px; border: 1px solid var(--border-soft);
  background: var(--surface-3); color: var(--text-muted);
  font-size: 0.75rem; padding: 0 12px; cursor: pointer;
}
.tb-icon-btn:hover { color: var(--text); background: var(--surface); }
.tb-icon-btn.active { color: var(--accent); border-color: var(--accent); }
.node-toolbar .node-close {
  margin-left: auto;
}
.tb-name {
  font-size: 0.78125rem; font-weight: 700; color: var(--text);
  background: transparent; border: 1px solid transparent; border-radius: 999px;
  padding: 6px 8px; width: 82px; min-width: 40px; flex-shrink: 2;
  font-family: var(--font-body);
}
.tb-name:hover, .tb-name:focus { background: var(--surface-3); border-color: var(--border-soft); outline: none; }
.node-head-name {
  flex: 1; width: auto;
}

.node-ref-chips {
  position: absolute; left: 10px; bottom: 44px; z-index: 4;
  display: flex; flex-wrap: wrap; gap: 4px; pointer-events: none;
}
.node-ref-chip {
  font-size: 0.625rem; padding: 3px 8px; border-radius: 999px;
  background: rgba(10, 10, 12, 0.75); border: 1px solid var(--border-soft);
  color: var(--text-faint);
}
.node-ref-chip.ready { color: var(--teal); border-color: var(--teal); }

.node-box {
  position: relative;
  width: 100%;
  aspect-ratio: var(--frame-ratio, 16 / 9);
  border-radius: var(--radius-lg);
  border: 1px solid var(--border);
  background: var(--surface-3);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.35);
  overflow: visible;
}
.node-box-inner {
  position: absolute; inset: 0;
  border-radius: inherit;
  overflow: hidden;
}
.node-box-media { position: absolute; inset: 0; }
.node-box-media img, .node-box-media video { width: 100%; height: 100%; object-fit: cover; display: block; }
.node-box-empty-glyph {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 1.375rem; color: var(--text-faint); pointer-events: none;
}
.node-box.has-media .node-box-empty-glyph { display: none; }

.node-box-prompt {
  position: absolute; left: 0; right: 0; top: 0;
  min-height: 34%;
  max-height: 70%;
  background: linear-gradient(to bottom, rgba(10, 10, 12, 0.85) 0%, rgba(10, 10, 12, 0.5) 65%, transparent 100%);
  color: var(--text);
  border: none; resize: none; outline: none;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: 10px 12px;
  font-size: 0.75rem; font-family: var(--font-body); line-height: 1.4;
  z-index: 3;
}
.node-box-prompt::placeholder { color: var(--text-faint); }
.node-box.has-media .node-box-prompt {
  min-height: 22%;
  font-size: 0.65625rem;
  background: linear-gradient(to bottom, rgba(10, 10, 12, 0.72) 0%, transparent 100%);
}

/* Workflow node's ✦ Enhance — icon-only, floating in the prompt overlay's
   top-right corner. No room on a compact node for full "Enhance"/
   "Enhancing…" label text, so state is shown via a spin + title tooltip
   instead (see wireEnhanceButton's iconOnly path). */
.node-enhance-btn {
  position: absolute;
  top: 8px;
  right: 10px;
  z-index: 4;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.25);
  background: rgba(10, 10, 12, 0.6);
  color: var(--text);
  font-size: 0.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.node-enhance-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.node-enhance-btn:disabled { cursor: default; opacity: 0.7; }
.node-enhance-btn.is-loading { animation: node-enhance-spin 0.9s linear infinite; }
@keyframes node-enhance-spin { to { transform: rotate(360deg); } }
.node-enhance-undo {
  position: absolute;
  top: 8px;
  right: 36px;
  z-index: 4;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.25);
  background: rgba(10, 10, 12, 0.6);
  color: var(--text-faint);
  font-size: 0.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.node-enhance-undo:hover { color: var(--text); border-color: var(--border-soft); }

/* --- prompt / reference nodes: unchanged simple head+body layout --- */

.node-head {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-bottom: 1px solid var(--border-soft);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  font-family: var(--font-body); font-weight: 700; font-size: 0.78125rem;
  color: var(--text);
  cursor: grab;
  touch-action: none;
}
.node-head:active { cursor: grabbing; }
.node-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--node-color, var(--accent)); flex-shrink: 0; }
.node-head-label { flex: 1; }
.node-head-meta { font-family: var(--font-mono); font-size: 0.65625rem; color: var(--text-faint); }
.node-close {
  background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: 0.8125rem;
  width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; border-radius: 50%;
}
.node-close:hover { color: var(--text); background: var(--surface-3); }

.node-body {
  padding: 12px; position: relative;
  background: var(--surface-2);
  border: 1px solid var(--border); border-top: none;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.35);
}
.node-body textarea, .node-body select { font-size: 0.78125rem; }
.node-body textarea { min-height: 90px; }
.node-select-row { display: flex; gap: 6px; margin-top: 8px; }
.node-select-row select { font-size: 0.71875rem; padding: 7px 8px; }

.node-port {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--surface-2); border: 2px solid var(--text-faint);
  display: flex; align-items: center; justify-content: center;
  color: var(--text-faint); font-size: 0.6875rem; line-height: 1;
  /* A normal pointer, never a crosshair/precision cursor, on the port
     itself or anywhere during an in-progress connection drag (see
     .node-canvas.connecting-line below) — connecting two nodes shouldn't
     feel any different, cursor-wise, from clicking any other control. */
  cursor: pointer;
  z-index: 6;
  touch-action: none;
}
.node-port::before {
  content: ''; position: absolute; inset: -17px; border-radius: 50%;
  touch-action: none;
}
.node-port:hover { border-color: var(--accent); color: var(--accent); }
.node-port-out { right: -8px; }
.node-port-in { left: -8px; }
.node-port.connected { background: var(--accent); border-color: var(--accent); color: var(--bg); }
.node-port.connect-target { border-color: var(--teal); box-shadow: 0 0 0 4px var(--teal-soft); }

.node-canvas-fab {
  position: absolute; right: 16px; bottom: 16px; z-index: 25;
  width: 48px; height: 48px; border-radius: 50%;
  background: var(--accent); color: #ffffff; border: none;
  font-size: 1.375rem; font-weight: 700; cursor: pointer;
  display: none; align-items: center; justify-content: center;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
}
@media (max-width: 860px) {
  .node-canvas-fab { display: flex; }
}

/* Content layer for nodes: this is what the zoom transform scales. The dot
   grid stays on .node-canvas itself (unscaled, always crisp); node-links
   (the SVG connection lines) also stays a direct sibling so its coordinates
   remain screen-space and don't need to be re-derived for zoom — only the
   nodes themselves live in this layer. */
.node-canvas-layer {
  position: absolute;
  top: 0;
  left: 0;
  /* Was inset:0 (i.e. exactly the visible viewport box) — meaning with zero
     nodes there was nothing anywhere that extended past the viewport, so
     .node-canvas (now overflow:auto, see its own comment) had a zero-size
     scrollable range and the Hand tool had nowhere to pan even a single
     pixel. A generous fixed floor here guarantees real pan room in every
     direction from an empty canvas; it still grows further, exactly as
     before, once actual nodes are positioned beyond this floor (an
     absolutely-positioned descendant with no clipping ancestor between here
     and .node-canvas still extends that scroll range on its own). */
  width: max(100%, 4000px);
  height: max(100%, 3000px);
  transform-origin: 0 0;
  z-index: 2;
  /* This div spans the whole canvas (inset:0) so its transform scales
     content from the canvas's own top-left corner, but most of that area
     is empty space between nodes. Without this, the layer's own (invisible)
     box would sit in front of the SVG connection layer everywhere — including
     in the gaps between nodes — and swallow all hover/click hit-testing for
     connection lines and their scissors buttons. Turning pointer-events off
     here and back on for the actual node elements lets clicks/hovers fall
     through to the SVG wherever there isn't a node on top. */
  pointer-events: none;
}
.node-canvas-layer .node { pointer-events: auto; }

/* ---------- zoom control ---------- */

.node-zoom-control {
  position: absolute; right: 16px; bottom: 16px; z-index: 26;
  display: flex; align-items: center; gap: 6px;
}
.node-zoom-side-btn {
  width: 32px; height: 32px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text-muted); font-size: 0.875rem; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
}
.node-zoom-side-btn:hover { border-color: var(--accent); color: var(--accent); }
@media (max-width: 860px) {
  .node-zoom-side-btn { display: none; }
}

/* ---------- canvas tool dock (Select / Hand) ---------- */

.wf-tool-dock {
  position: absolute; left: 16px; top: 50%; transform: translateY(-50%); z-index: 26;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 999px; padding: 6px;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
}
.wf-tool-btn {
  position: relative;
  width: 32px; height: 32px; border-radius: 999px;
  background: transparent; border: 1px solid transparent;
  color: var(--text-muted); font-size: 0.875rem; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.12s ease, color 0.12s ease;
}
.wf-tool-btn:hover { color: var(--text); }
.wf-tool-icon { pointer-events: none; }
/* Subtle shortcut-letter badge (V / H), pinned to the button's corner so the
   dock stays the same compact pill size — this is helper text, not a resize. */
.wf-tool-key {
  position: absolute; right: -3px; bottom: -3px;
  min-width: 12px; padding: 0 2px; border-radius: 4px;
  background: var(--surface-2); border: 1px solid var(--border-soft);
  color: var(--text-faint); font-size: 0.5rem; font-weight: 700; line-height: 1.3;
  font-family: var(--font-mono); text-align: center;
  pointer-events: none;
}
.wf-tool-btn.active .wf-tool-key { background: #fff; color: #2a2a2e; border-color: #fff; }
.wf-tool-btn.active {
  /* Was color: #ffffff — invisible against this button's own white active
     background. Only the Select tool's "↖" glyph is actually drawn with
     this `color` (the Hand tool's "✋" is an emoji glyph rendered by the
     OS emoji font in its own fixed color, so this change has no visible
     effect on it). Dark graphite, not pure black, to sit naturally on the
     white pill without changing the pill's background/size/position. */
  background: #fff; color: #2a2a2e;
}
@media (max-width: 860px) {
  .wf-tool-dock { display: none; }
}

.node-canvas.hand-tool-active { cursor: grab; }
.node-canvas.hand-tool-active.panning { cursor: grabbing; }
/* Spacebar temporary-pan works the same way regardless of the selected
   tool, without needing the Hand tool's own toolbar state to change. */
.node-canvas.space-pan-active { cursor: grab; }
.node-canvas.space-pan-active.panning { cursor: grabbing; }

/* Hand tool must read as pannable over the COMPLETE node surface — header,
   body, prompt textarea, media preview, footer, ports, and every preset/
   control inside it — not just the empty canvas around it. Selects,
   buttons, and text areas all carry their own cursor rules (pointer, text)
   that would otherwise win by specificity, so this reaches every descendant
   with !important. The actual click/drag suppression that keeps this from
   also opening a dropdown or pressing a button lives in app.js; this block
   is purely the visual cursor half of that same requirement. */
.node-canvas.hand-tool-active .node,
.node-canvas.hand-tool-active .node *,
.node-canvas.space-pan-active .node,
.node-canvas.space-pan-active .node * {
  cursor: grab !important;
}
.node-canvas.hand-tool-active.panning .node,
.node-canvas.hand-tool-active.panning .node *,
.node-canvas.space-pan-active.panning .node,
.node-canvas.space-pan-active.panning .node * {
  cursor: grabbing !important;
}

/* Safety net for "always a normal pointer while connecting, never a
   crosshair/precision cursor": forces the default arrow across the whole
   canvas for the duration of a connection drag, regardless of whatever
   individual hover cursor (text caret, pointer, etc.) an element under the
   pointer would otherwise show. Toggled via workflow.connecting in app.js. */
.node-canvas.connecting-line,
.node-canvas.connecting-line * {
  cursor: default !important;
}
.node-zoom-pill {
  display: flex; align-items: center; gap: 6px;
  background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text); font-size: 0.78125rem; font-weight: 600;
  padding: 7px 12px; border-radius: 999px; cursor: pointer;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
}
.node-zoom-pill:hover { border-color: var(--accent); }
.node-zoom-caret { color: var(--text-faint); font-size: 0.625rem; }
.node-zoom-menu {
  position: absolute; right: 0; bottom: calc(100% + 8px);
  min-width: 190px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 10px; padding: 6px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
  display: flex; flex-direction: column; gap: 2px;
}
.node-zoom-menu.hidden { display: none; }
.node-zoom-menu button {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: transparent; border: none; color: var(--text);
  font-size: 0.78125rem; text-align: left; padding: 7px 8px; border-radius: 6px; cursor: pointer;
}
.node-zoom-menu button:hover { background: var(--surface-3); }
.node-zoom-hint { color: var(--text-faint); font-size: 0.6875rem; font-family: var(--font-mono); }
.node-zoom-tips {
  display: flex; flex-direction: column; gap: 4px;
  margin-top: 6px; padding: 8px 8px 4px;
  border-top: 1px solid var(--border);
}
.node-zoom-tips span { display: flex; align-items: center; gap: 8px; font-size: 0.71875rem; color: var(--text-faint); }
.node-zoom-tips .node-zoom-hint { color: var(--text-muted); }

@media (max-width: 860px) {
  .node-zoom-control { right: 76px; }
}

/* ---------- connection delete (scissors) ---------- */

.node-connect-hit { cursor: pointer; }

/* The scissors buttons live in their own plain-HTML layer (not inside the
   SVG as a <foreignObject>) — SVG foreignObject hit-testing for click
   events proved unreliable (elementFromPoint would resolve to the SVG path
   underneath it instead of the foreignObject's own content, even while its
   CSS :hover state was active), so this is the more robust of the two
   approaches the spec allows. */
.node-scissors-layer {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
.node-connect-scissors {
  position: absolute;
  width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text); font-size: 0.6875rem;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
  opacity: 0; pointer-events: none;
  transition: opacity 0.12s;
}
.node-connect-scissors.visible { opacity: 1; pointer-events: auto; }
.node-connect-scissors:hover { background: var(--accent); border-color: var(--accent); color: #ffffff; }

/* ---------- progress / spinner ---------- */

.btn-progress-wrap {
  display: none;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}
.btn-progress-wrap.active { display: flex; }
.btn-spinner {
  width: 14px; height: 14px; border-radius: 50%;
  /* Silver ring, black head. NOT var(--border): that token and --accent are
     both #0A0A0A here, which made every edge the same colour and the spin
     invisible. The two colours must be stated independently. */
  border: 2px solid var(--surface-3);
  border-top-color: var(--accent);
  animation: spin 0.7s linear infinite;
  flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
.progress-track {
  flex: 1; height: 4px; border-radius: 2px;
  background: var(--surface-3);
  overflow: hidden;
}
.progress-track-fill {
  width: 40%; height: 100%; border-radius: 2px;
  background: linear-gradient(90deg, var(--accent), var(--gold));
  animation: progress-slide 1.1s ease-in-out infinite;
}
@keyframes progress-slide {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
@keyframes busy-pulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  /* Overrides the global * rule above — same !important, but a real
     selector outranks the universal one. */
  .btn-spinner,
  .progress-track-fill,
  .thumb-pending-spinner,
  .primary-btn.loading::after,
  .primary-btn.cancel-mode::after,
  .node-generate-btn.loading::after {
    animation-name: busy-pulse !important;
    animation-duration: 1.6s !important;
    animation-timing-function: ease-in-out !important;
    animation-iteration-count: infinite !important;
    /* Nothing rotates or travels; only the opacity changes. */
    transform: none !important;
  }
  /* An indeterminate bar with no motion has to span the track, or the 40%
     stub just looks like a job frozen at 40% complete. */
  .progress-track-fill { width: 100% !important; }
  /* Nothing travels across the Cancel bar; the highlight parks mid-button as
     a static sheen so the button still reads as busy. */
  .primary-btn.cancel-mode {
    animation: none !important;
    background-position: 50% 0, 0 0 !important;
  }
}
.primary-btn.loading, .node-generate-btn.loading {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}
/* Cancel-capable Generate buttons — Image / Video / Upscale (image + video).
   While one of these is generating, the Generate button itself becomes a
   live, clickable Cancel button instead of the disabled spinner-only
   .loading state used everywhere else — see armGenerateCancel/
   disarmGenerateCancel in app.js. Deliberately NOT combined with .loading:
   this state stays clickable (pointer-events unset) and keeps its label
   readable. */
.primary-btn.cancel-mode {
  position: relative;
  /* Two background layers: a travelling highlight over the base red. The
     highlight is layer 1 so it sits above the base fill but still behind all
     content. */
  background-color: #4a1010 !important;
  background-image:
    linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.22) 50%, transparent 100%),
    linear-gradient(135deg, #7a1f1f, #4a1010) !important;
  background-size: 42% 100%, 100% 100% !important;
  background-repeat: no-repeat, no-repeat !important;
  color: #ffd6d6 !important;
  box-shadow: 0 6px 20px -6px rgba(0, 0, 0, 0.55), var(--inset-hairline);
  animation: cancel-sweep 1.15s ease-in-out infinite;
}
/* Spinner, sitting before the CANCEL label rather than replacing it. Colours
   are stated outright, not taken from --border/--accent: those two are the
   same value in this theme, which is exactly what made the other spinner a
   static dot. */
.primary-btn.cancel-mode::after {
  content: '';
  position: absolute;
  left: 14px; top: 50%;
  width: 13px; height: 13px; margin-top: -7px;
  border-radius: 50%;
  border: 2px solid rgba(255, 214, 214, 0.32);
  border-top-color: #ffd6d6;
  animation: spin 0.7s linear infinite;
  pointer-events: none;
}
/* Only the highlight layer moves; the base fill is pinned. */
@keyframes cancel-sweep {
  0%   { background-position: -60% 0, 0 0; }
  100% { background-position: 160% 0, 0 0; }
}
.primary-btn.cancel-mode:hover { filter: brightness(1.12); }
/* Cancel stays armed while the backend cancellation resolves, so the button
   is disabled but the job is still genuinely running — the feedback has to
   keep going until the poller sees a terminal status. */
.primary-btn.cancel-mode:disabled { opacity: 0.7; cursor: wait; }
.primary-btn.loading::after, .node-generate-btn.loading::after {
  content: '';
  position: absolute; left: 50%; top: 50%;
  width: 16px; height: 16px; margin: -8px 0 0 -8px;
  border-radius: 50%;
  border: 2px solid rgba(0, 0, 0, 0.25);
  border-top-color: #ffffff;
  animation: spin 0.7s linear infinite;
}
.node-generate-btn.loading::after {
  border: 2px solid rgba(0, 0, 0, 0.25);
  border-top-color: #ffffff;
  width: 12px; height: 12px; margin: -6px 0 0 -6px;
}

/* ---------- click-to-zoom image lightbox ---------- */
.image-lightbox-backdrop {
  /* Above absolutely everything else in the app — including the mobile
     fixed top bar's z-index: 9999 !important — so the viewer (and its
     close button) is always the top layer, on every page, on every
     viewport size. */
  display: none; position: fixed; inset: 0; z-index: 10000;
  background: rgba(0, 0, 0, 0.85); align-items: center; justify-content: center;
  padding: 40px; cursor: zoom-out;
}
.image-lightbox-backdrop.open { display: flex; }
.image-lightbox-backdrop img,
.image-lightbox-backdrop video {
  max-width: 90vw; max-height: 90vh; border-radius: 10px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
  cursor: default;
}

/* Explicit Close (X) control — always present, always on top, so a media
   viewer (image or video, any page) never becomes a dead end. Sized well
   past the 44px touch-target minimum, positioned clear of notches/safe
   areas, and given a solid scrim + border so it reads clearly against
   both dark and bright media underneath it. */
.image-lightbox-close {
  position: fixed;
  top: max(16px, env(safe-area-inset-top));
  right: max(16px, env(safe-area-inset-right));
  z-index: 10001;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: rgba(20, 18, 16, 0.78);
  border: 1.5px solid var(--accent, #101012);
  color: #fff;
  font-size: 1.25rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.55);
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.image-lightbox-close:hover,
.image-lightbox-close:focus-visible {
  background: rgba(20, 18, 16, 0.92);
  border-color: #fff;
}
.image-lightbox-close:active { transform: scale(0.94); }

/* Minimal-controls video mode (Explore only): native <video> controls are
   stripped entirely, so this is the ONLY playback control in the enlarged
   viewer — a single Play/Pause toggle, centered on the video, alongside the
   Close (X) button above. No scrubber, volume, fullscreen, or download UI. */
.image-lightbox-playpause {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 10001;
  width: 64px; height: 64px; border-radius: 50%;
  background: rgba(20, 18, 16, 0.72); border: 1.5px solid rgba(255, 255, 255, 0.85);
  color: #fff; display: flex; align-items: center; justify-content: center;
  cursor: pointer; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
  transition: background 0.15s ease, opacity 0.15s ease;
  -webkit-tap-highlight-color: transparent;
}
.image-lightbox-playpause[hidden] { display: none; }
.image-lightbox-playpause::after {
  content: ''; width: 0; height: 0;
  border-style: solid; border-width: 11px 0 11px 18px;
  border-color: transparent transparent transparent #fff;
  margin-left: 4px;
}
.image-lightbox-playpause.is-playing::after {
  border-style: none; width: 8px; height: 22px;
  border-left: 7px solid #fff; border-right: 7px solid #fff; margin-left: 0;
}
.image-lightbox-playpause:hover { background: rgba(20, 18, 16, 0.9); }

@media (max-width: 640px) {
  .image-lightbox-backdrop { padding: 16px; }
  .image-lightbox-close {
    /* A bit larger still on touch screens, and pulled fully clear of any
       device notch/status bar via the safe-area inset above. */
    width: 48px;
    height: 48px;
    font-size: 1.3125rem;
  }
}

/* History card thumbnails: click-to-enlarge + hover delete button */
.entity-card .thumb { cursor: zoom-in; }

/* ---------- library picker modal ---------- */

.lib-modal-backdrop {
  display: none;
  /* THE actual root cause of the "can't reach the close button, scrolling
     never reveals it" report: the mobile fixed top bar (.rail, further up
     this file) is `position:fixed` at `z-index: 9999 !important`, and this
     backdrop was sitting at only 1000 — well UNDER it. Every fix to this
     modal's own internal sizing/scrolling (the dvh/flex-shell work above)
     was correct but couldn't matter: the modal's own header rendered
     directly behind the opaque, always-on-top rail bar, which no amount of
     scrolling can ever move out of the way, because the rail bar isn't
     part of the modal's scroll content at all — it's a separate fixed
     layer painted on top of everything, permanently. 10000 matches the
     precedent already set by .image-lightbox-backdrop below for this exact
     same reason (see its comment) — "above absolutely everything else,
     including the mobile top bar, on every viewport size". */
  position: fixed; inset: 0; z-index: 10000;
  background: rgba(0, 0, 0, 0.6);
  align-items: center; justify-content: center;
  padding: 20px;
}
.lib-modal-backdrop.open { display: flex; }
.lib-modal {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  width: 100%; max-width: 560px;
  max-height: 80vh;
  display: flex; flex-direction: column;
}
.lib-modal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; border-bottom: 1px solid var(--border-soft);
  font-weight: 700; font-size: 0.875rem;
}
.lib-modal-close {
  background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: 1rem;
}
.lib-modal-grid {
  padding: 14px 16px;
  overflow-y: auto;
  overflow-x: hidden;
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: auto;
  gap: 12px;
  align-items: start;
  /* Smooth momentum scrolling on iOS, and — critically — stop that
     momentum from "chaining" into the page behind once the grid hits its
     top/bottom edge. Without overscroll-behavior, an iOS rubber-band at the
     end of the grid's scroll range hands the gesture to whatever's
     underneath, which is exactly the mechanism that made the close button
     feel unreachable: the grid LOOKS like the scroll owner, but the page
     was quietly scrolling too. This makes .lib-modal-grid the one, single,
     intentional vertical scroll owner for Library content. */
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
@media (max-width: 640px) {
  .lib-modal-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Mobile Library modal fix: on a phone, `vh` includes space the browser
   chrome (URL bar etc.) can cover, so a modal sized off `vh` alone can be
   taller than what's actually visible — with the backdrop centering it
   (align-items:center) and never scrolling itself, the top of the modal
   (its header + close button, per .lib-modal-head below, which is already
   a normal flex-column child sitting above the scrollable .lib-modal-grid)
   can end up pushed above the top of the screen with no way to reach it.
   `dvh` (dynamic viewport height) tracks the ACTUAL visible area and is
   layered on top of the `vh` fallback for browsers that don't support it —
   the second declaration simply wins where it's understood. Anchoring the
   backdrop to the bottom edge on mobile (flex-end, no padding) keeps the
   header flush with the top of whatever height the modal is actually
   given, rather than centered and potentially clipped on both ends. */
@media (max-width: 860px) {
  .lib-modal-backdrop {
    padding: 0;
    align-items: flex-end;
  }
  .lib-modal {
    width: 100%;
    max-width: none;
    border-radius: 16px 16px 0 0;
    max-height: 90vh;
    max-height: 90dvh;
    /* Keeps the close button clear of a phone's rounded corners/home
       indicator without adding any visible gap on devices without one. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .lib-modal-head {
    /* Never scrolls away — .lib-modal-grid below is the only scrolling
       region (flex:1; overflow-y:auto; min-height:0, already set above),
       so the close button stays reachable no matter how far the user has
       scrolled the grid. */
    flex-shrink: 0;
    padding-top: max(14px, env(safe-area-inset-top, 0px));
  }
}

/* Background scroll lock while ANY full-screen overlay modal is open on
   mobile — the Library picker AND the whole .modal-overlay family (New
   Project, Confirm, Elements Library, Elements Picker, New Element) all
   share this one class, driven by the single shared lockBodyScroll()/
   unlockBodyScroll() pair in app.js (see watchModalOverlayLock()), so a
   fix here or there can never drift out of sync with whichever modal —
   or whichever path closed it: a close button, a backdrop click-outside,
   or a multi-step flow that swaps one modal for another. `overflow:hidden`
   alone does NOT reliably stop background scrolling on iOS Safari — a
   touchmove on the page behind a fixed-position modal still drags the
   document there, which is what let the page (not the modal's own content)
   end up as the thing scrolling, leaving a tall modal's header/close button
   visually adrift regardless of the modal's own internal layout.
   `position:fixed` on body (set in app.js, alongside this class) is what
   actually blocks that. Scoped to mobile only — desktop's centered modals
   never let the page behind them visibly scroll in the first place, so
   locking it there would be a desktop behavior change with no matching bug
   to fix. */
@media (max-width: 860px) {
  body.modal-scroll-locked {
    position: fixed;
    left: 0;
    right: 0;
    overflow: hidden;
    /* Defensive: caps body at exactly the viewport width while locked, so
       there's never any horizontal scroll room for iOS's focused-input
       auto-scroll-into-view behavior to push the fixed body sideways in
       (the actual bug this modal hit came from a <16px input font-size
       triggering iOS's auto-zoom instead — see .modal-input above — but
       this closes off the same visual symptom from recurring via any other
       future input in a locked modal). */
    max-width: 100vw;
    overflow-x: hidden;
    /* top offset and width are set inline by app.js at lock time, since
       they depend on the page's actual scroll position/width at the
       moment a modal opens — see watchModalOverlayLock() in app.js. */
  }
}
/* Strict row-and-column grid — every card is a normal in-flow grid item
   (no absolute positioning, no negative margins, no transforms, no
   column-count/masonry) with its own square thumbnail cell, so rows always
   fully finish before the next one begins regardless of image size. */
.lib-modal-item {
  /* Square sizing via the padding-bottom trick, NOT aspect-ratio. This grid
     lives in a scrollable, flex-sized container (.lib-modal-grid is a
     flex:1 child with overflow-y:auto), and in that combination
     `aspect-ratio` on a `grid-auto-rows: auto` item doesn't correctly
     contribute to the row's auto height in every browser: each item still
     measures 118.75×118.75 individually, but the row track was only being
     given ~94px, so every second row visually overlapped the one above it
     by ~24px — the exact "library modal still overlapping" bug. Padding
     percentages resolve against the item's own resolved width regardless
     of row-track timing, so this sizing can never be shorted by that
     grid/aspect-ratio interaction. */
  position: relative;
  min-width: 0;
  width: 100%;
  height: 0;
  padding-bottom: 100%;
  border-radius: var(--radius);
  overflow: hidden;
  border: 2px solid transparent;
  cursor: pointer;
  background: var(--surface-3);
  margin: 0;
  top: auto; left: auto;
  transform: none;
}
.lib-modal-item img,
.lib-modal-item video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: cover;
  display: block;
  border-radius: inherit;
}
.lib-modal-item:hover { border-color: var(--accent); }
.lib-modal-empty { padding: 30px; text-align: center; color: var(--text-faint); font-size: 0.8125rem; grid-column: 1 / -1; }
.lib-pick-btn {
  background: var(--surface-3); border: 1px solid var(--border); color: var(--text-muted);
  font-size: 0.71875rem; padding: 6px 10px; border-radius: var(--radius); cursor: pointer;
  margin-top: 6px;
}
.lib-pick-btn:hover { color: var(--text); border-color: var(--accent); }

/* ---------- new project modal ---------- */

.modal-overlay {
  display: none;
  /* Same fix, same reason as .lib-modal-backdrop above: the mobile fixed
     top bar (.rail) is z-index 9999 !important, so at 1000 every one of
     these overlays (New Project, Confirm, Elements Library, Elements
     Picker, New Element) had its own header/close button permanently
     hidden behind that bar on mobile — no scroll fix could ever reach it,
     since the rail bar is a separate always-on-top fixed layer, not part
     of the modal's own scrollable content. 10000 matches the precedent set
     by .image-lightbox-backdrop further down this file. */
  position: fixed; inset: 0; z-index: 10000;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  align-items: center; justify-content: center;
  padding: 20px;
}
.modal-overlay.open { display: flex; }
.modal-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  width: 100%; max-width: 420px;
  padding: 20px 22px 22px;
}
.modal-card-head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 14px;
}
.modal-eyebrow {
  font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 700;
  letter-spacing: 0.8px; text-transform: uppercase; color: var(--accent);
}
.modal-close {
  background: none; border: none; color: var(--text-faint); cursor: pointer;
  font-size: 1.25rem; line-height: 1; padding: 0;
}
.modal-close:hover { color: var(--text); }

/* Mobile modal-shell fix — same root cause and same fix pattern as the
   Library picker modal (.lib-modal) above: `.modal-card` had no max-height
   at all, so a card with a lot of content (the Elements Library grid of
   portrait thumbnails is the real-world case that trapped a user — dozens
   of tall images with `align-items:center` on the backdrop centering a card
   taller than the screen) pushed its own header/close button above the top
   of the viewport with nothing to scroll to reach it. Constrains every
   `.modal-card` to the actual usable mobile viewport (`dvh`, not just
   `vh`), turns it into a flex column so its own header can never be part of
   whatever scrolls, and accounts for the safe-area insets on notched
   phones. Applies to every `.modal-overlay` card (New Project, Confirm,
   Elements Library, Elements Picker, New Element) — the exact same
   structural bug can happen to any of them once their content is long
   enough, not just the Elements Library. */
@media (max-width: 860px) {
  .modal-overlay {
    padding: 0;
    align-items: flex-end;
  }
  .modal-card {
    width: 100%;
    max-width: none;
    border-radius: 16px 16px 0 0;
    max-height: 90vh;
    max-height: 90dvh;
    display: flex;
    flex-direction: column;
    /* Generic fallback scroll owner for modal-cards with no dedicated
       internal scroll region of their own (New Project, Confirm, New
       Element) — content that's too tall for the card scrolls the whole
       card rather than overflowing past the viewport. .elements-modal-card
       below overrides this to `hidden` because IT has a dedicated internal
       scroll owner (.elements-grid) and must not compete with it. */
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .modal-card-head {
    /* Never scrolls away — it's a flex-column child sitting above whichever
       element actually scrolls (either .modal-card itself, or, for the
       Elements modals, .elements-grid — see the .elements-modal-card
       override further down this file, placed after .elements-grid's own
       base rule so it actually wins), so the close button stays reachable
       no matter how far the user has scrolled the content. */
    flex: 0 0 auto;
    padding-top: max(0px, env(safe-area-inset-top, 0px));
  }
}
.modal-heading {
  font-family: var(--font-display); font-weight: 800; font-size: 1.375rem;
  margin: 0 0 16px; color: var(--text);
}
.modal-input {
  width: 100%; background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); color: var(--text); padding: 11px 13px;
  font-family: var(--font-body); font-size: 0.875rem; outline: none;
}
.modal-input:focus { border-color: var(--accent); }
.modal-input::placeholder { color: var(--text-faint); }
/* iOS Safari auto-zooms the whole page in when a focused input's font-size
   is under 16px, "to make it legible" — this is what actually broke the
   New Project modal's Create button: typing into the 14px "Name your
   project" field triggered that auto-zoom, which visually shifted/cropped
   the entire viewport (the modal AND the dimmed page behind it, matching
   the screenshot exactly) rather than anything about this modal's own
   layout. 16px is the documented threshold iOS won't zoom past, so this
   keeps every modal text input at 14px visually on desktop while forcing
   16px specifically on mobile, where the bug actually happens. */
@media (max-width: 860px) {
  .modal-input { font-size: 1rem; }
}
.modal-actions {
  display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;
}
.modal-btn-cancel {
  background: none; border: 1px solid var(--border); color: var(--text-muted);
  font-family: var(--font-mono); font-size: 0.78125rem; padding: 9px 16px;
  border-radius: var(--radius); cursor: pointer;
}
.modal-btn-cancel:hover { color: var(--text); border-color: var(--text-muted); }
.modal-btn-primary {
  background: var(--accent); border: 1px solid var(--accent); color: #ffffff;
  font-family: var(--font-mono); font-weight: 700; font-size: 0.78125rem; padding: 9px 18px;
  border-radius: var(--radius); cursor: pointer;
}
.modal-btn-primary:disabled {
  background: var(--surface-3); border-color: var(--border); color: var(--text-faint);
  cursor: not-allowed;
}
.modal-btn-danger {
  background: var(--danger-soft); border: 1px solid var(--danger); color: var(--danger);
  font-family: var(--font-mono); font-weight: 700; font-size: 0.78125rem; padding: 9px 18px;
  border-radius: var(--radius); cursor: pointer;
}
.modal-btn-danger:hover { background: var(--danger); color: #fff; }

/* ---------- project hub ---------- */

.project-hub-back {
  display: inline-block; font-family: var(--font-mono); font-size: 0.71875rem;
  color: var(--text-faint); text-decoration: none; margin-bottom: 10px;
}
.project-hub-back:hover { color: var(--accent); }

.project-hub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(285px, 330px));
  gap: 36px;
  max-width: none;
  margin: 8px auto 0;
  justify-content: center;
  padding-left: 16px;
}
main:has(> #view-studio-hub.active),
main:has(> #view-project-hub.active) {
  max-width: none;
}
.project-hub-tile {
  aspect-ratio: 1 / 1;
  display: flex; flex-direction: column;
  gap: 0;
  cursor: pointer;
  border-radius: var(--radius-lg, 14px);
  overflow: hidden;
  transition: transform 0.15s ease;
  /* .project-hub-tile also carries the generic .entity-card class, which
     brings its own border/background/box-shadow (the "card" look). Kill
     those explicitly here so the tile is just the video + label, no box. */
  background: none;
  border: none;
  box-shadow: none;
}
.project-hub-tile:hover {
  transform: translateY(-3px);
  border-color: transparent;
  box-shadow: none;
}
.project-hub-tile-icon {
  font-size: 2.125rem;
  color: var(--accent);
  width: 100%;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  box-sizing: border-box;
  border-radius: var(--radius-lg, 14px);
}
.project-hub-tile-icon video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
}
.project-hub-tile-label {
  font-family: var(--font-display); font-weight: 700; font-size: 0.875rem;
  letter-spacing: 0.6px;
  text-transform: uppercase; /* every tile title is set in caps, whatever the markup says */
  color: var(--text);
  text-align: center;
  padding: 0 0 20px;
  position: relative;
}
.project-hub-tile-label::after {
  content: '';
  display: block;
  width: 22px;
  height: 2px;
  background: var(--accent);
  margin: 8px auto 0;
  border-radius: 1px;
}
@media (max-width: 720px) {
  .project-hub-grid { gap: 20px; grid-template-columns: repeat(auto-fill, minmax(225px, 1fr)); }
}
@media (max-width: 440px) {
  .project-hub-grid { grid-template-columns: 1fr; }
}

/* Project-start screen (the two-choice Studio Agent / Workflow screen a
   just-created project lands on) — reuses the exact same tile family,
   dark Ember canvas, and studio-frame layout as the Studio icon grid below
   (#view-studio-hub), just with exactly two centered tiles instead of the
   full tool list. See the "Studio hub + Project start" shared block right
   after this one — that's what actually paints the background/frame/tile
   treatment for this screen; this block only sets the column count. */
.project-hub-grid-start {
  grid-template-columns: repeat(auto-fit, minmax(225px, 330px));
  justify-content: center;
}
.project-hub-tile-desc {
  font-family: var(--font-body, inherit);
  font-weight: 400;
  font-size: 0.78125rem;
  text-align: center;
  padding: 0 18px 20px;
  margin-top: -12px;
  line-height: 1.4;
}
@media (max-width: 440px) {
  .project-hub-grid-start { grid-template-columns: 1fr; max-width: 340px; margin-left: auto; margin-right: auto; }
}

/* ---------- Studio hub + Project start: workspace frame, atmosphere, tile edges ----------
   Shared by #view-studio-hub (the full Studio tool grid) and #view-project-hub
   (the Studio Agent / Workflow project-start screen) — both use the exact
   same tile family, so both get the exact same dark Ember canvas, header
   treatment, and "natural metallic tile, no extra frame" icon handling. The
   grid itself (.project-hub-grid — columns, gap, tile size/order) is set
   separately for each below; only the space around it and the tiles' own
   edges/labels are shared here. */

/* The whole content area becomes one continuous dark Ember canvas: dots
   span the full view (header included), not just the grid wrapper. No
   outer box anywhere — open floating-gallery feel, per the page background
   rather than a panel. */
:root { --studio-black: #000000; }

/* Every layer behind the grid — the workspace column itself, the view, the
   (layout-only) frame wrapper, the grid, and each tile's video container —
   shares this exact value. No layer may use --bg, --surface, or any other
   near-black; that's what was producing the visible seam. */
/* Full-bleed canvas: lift main's own centered max-width/padding cap for
   these views (same technique used for #view-workflow / #view-home above)
   so the dotted background can reach both outer edges instead of stopping
   at a centered 1180px column with plain black margins on either side. The
   content itself (panel-head, studio-frame) gets that same 1180px/44px
   measurement re-applied directly below so the tiles render at precisely
   the same size and position as before — only the background layer
   actually becomes full-bleed. */
main:has(> #view-studio-hub.active),
main:has(> #view-project-hub.active) {
  background: var(--studio-black);
  max-width: none;
  padding: 0;
}
#view-studio-hub,
#view-project-hub {
  position: relative;
  background: var(--studio-black);
  min-height: 100%;
}
/* Dots: tiny, evenly spaced, very low-contrast — neutral soft silver-gray
   (not the dark dots used on the silver pages, which would be invisible
   against black) so this page still reads as part of the same dotted
   visual language, just inverted for a dark background. No mask/fade:
   the pattern stays uniform all the way to every edge, right up to the
   sidebar on the left and the viewport edge on the right. */
#view-studio-hub::before,
#view-project-hub::before {
  content: '';
  position: absolute; inset: 0;
  background-image: radial-gradient(rgba(180, 186, 189, 0.14) 1px, transparent 1px);
  background-size: 22px 22px;
  pointer-events: none;
  z-index: 0;
}
/* No ambient glow layer on this page — the background is clean neutral
   black. Title/subtitle/underline colors are set explicitly below so
   nothing here relies on the app-wide (black) --accent token, which would
   disappear against this page's black background. */
#view-studio-hub > *,
#view-project-hub > * { position: relative; z-index: 1; }

/* main's own centered padding/max-width is lifted above so the dotted
   background can go full-bleed. This wrapper restores that exact same
   36/44/60px padding + 1180px max-width around the actual content (panel
   head + tile grid) so the tiles render at precisely the same size and
   internal spacing they always have — .panel-head, .studio-frame and
   .project-hub-grid below are completely unchanged. The one deliberate
   difference from before: this wrapper is centered (margin-left/right:auto)
   instead of flush against the sidebar, so the balanced "equal space either
   side of the grid" the background full-bleed now calls for is actually
   there — previously main itself capped at 1180px flush-left, which looked
   fine only because the leftover space past 1180px was outside main and
   uninstrumented; now that main reaches the true edge, that same flush-left
   placement would strand the grid against the sidebar with a large dead
   zone on the right on wide screens, so it's centered here instead. */
/* Header (Back button, title, subtitle) is its OWN layout region: plain
   padding at the standard page margin (matches main's original 36/44px),
   flush left against the sidebar — deliberately NOT centered and NOT
   sharing the grid's max-width container. */
#view-studio-hub .panel-head,
#view-project-hub .panel-head {
  position: relative;
  box-sizing: border-box;
  padding: 36px 44px 20px;
}
@media (max-width: 640px) {
  #view-studio-hub .panel-head,
  #view-project-hub .panel-head { padding: 20px 14px 20px; }
}
#view-studio-hub .panel-head::after,
#view-project-hub .panel-head::after {
  content: '';
  position: absolute; left: 44px; right: 44px; bottom: 0;
  height: 1px;
  background: linear-gradient(90deg, var(--border-soft), transparent 75%);
}
@media (max-width: 640px) {
  #view-studio-hub .panel-head::after,
  #view-project-hub .panel-head::after { left: 14px; right: 14px; }
}
#view-studio-hub .panel-head h1,
#view-project-hub .panel-head h1 { letter-spacing: 0.3px; color: #ffffff; }
#view-studio-hub .panel-head .panel-sub,
#view-project-hub .panel-head .panel-sub { color: rgba(255, 255, 255, 0.62); }
/* "← All Projects" back link — same dark-canvas visibility treatment as the
   title/subtitle above; the link's default color reads as near-black,
   which is invisible on this page's black background. */
#view-project-hub .project-hub-back { color: rgba(255, 255, 255, 0.62); }
#view-project-hub .project-hub-back:hover { color: #ffffff; }
/* Tile description copy (Studio Agent / Workflow's one-line explainer) —
   same treatment: default --text-faint is a dark color meant for the
   silver pages, invisible here. */
#view-project-hub .project-hub-tile-desc { color: rgba(255, 255, 255, 0.62); }

/* Tile grid is a SEPARATE layout region from the header: its own max-width
   container, independently centered under the sidebar-to-edge workspace.
   1428px = 4 tiles (330px) + 3 gaps (36px) — exactly the 4/4/3 row width;
   the project-start screen's 2-tile row fits comfortably within the same
   frame width, centered the same way. */
#view-studio-hub .studio-frame,
#view-project-hub .studio-frame {
  position: relative;
  box-sizing: border-box;
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 0;
  max-width: 1516px;
  margin-left: auto;
  margin-right: auto;
  padding: 28px 44px 60px;
  overflow: visible;
}

/* Only the video's own container gets the solid matching black — the tile
   wrapper and the grid itself stay transparent so the dots show through in
   the gaps BETWEEN tiles too, instead of being blocked by an opaque
   rectangle sitting over them there. */
#view-studio-hub .project-hub-tile,
#view-studio-hub .project-hub-grid,
#view-project-hub .project-hub-tile,
#view-project-hub .project-hub-grid {
  background: none;
}
/* Studio hub: fixed 4-column grid tracks so an incomplete last row
   (3 tiles) lands in columns 1-3, aligned exactly under the rows above,
   with column 4 left empty — not centered as its own row. */
/* Responsive tile grid: auto-fit + minmax lets the browser pick however
   many 330px-ish columns actually fit the available width (4 on a wide
   desktop, naturally fewer as the workspace narrows — e.g. under the Studio
   Agent drawer or on a 1024-1366px laptop) instead of a hardcoded 4-column
   track that simply overflows/clips once the container is narrower than
   4*330px + gaps. Tile width now tracks its grid column (100%) rather than
   a fixed px, so tiles shrink together with the column instead of forcing
   a horizontal scrollbar. */
#view-studio-hub .project-hub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(225px, 330px));
  justify-content: center;
  max-width: none;
  margin: 8px 0 0;
  padding-left: 0;
}
#view-studio-hub .project-hub-tile {
  width: 100%;
  min-width: 0;
}
@media (max-width: 440px) {
  #view-studio-hub .project-hub-grid { grid-template-columns: 1fr; }
  #view-studio-hub .project-hub-tile { width: 100%; }
}
/* Project start: same tile size/shape as Studio hub's grid (330px, 1:1),
   just laid out as exactly two centered columns instead of four — see
   .project-hub-grid-start above for the column count itself. */
#view-project-hub .project-hub-grid {
  display: grid;
  max-width: none;
  margin: 8px 0 0;
  padding-left: 0;
  gap: 36px;
}
#view-project-hub .project-hub-tile {
  width: 330px;
  aspect-ratio: 1 / 1;
}
@media (max-width: 720px) {
  #view-project-hub .project-hub-grid { gap: 20px; }
  #view-project-hub .project-hub-tile { width: 225px; }
}
@media (max-width: 440px) {
  #view-project-hub .project-hub-tile { width: 100%; }
}
#view-studio-hub .project-hub-tile-icon,
#view-project-hub .project-hub-tile-icon {
  position: relative;
  /* No outer placard frame — the icon artwork carries its own border, so
     this container is transparent with no border/background/shadow of its
     own (avoids a double-frame effect around each icon). */
  background: none;
  border: none;
  box-shadow: none;
}
#view-studio-hub .project-hub-tile:hover,
#view-project-hub .project-hub-tile:hover {
  /* The tile itself must not move — only its label reacts. */
  transform: none;
}

#view-studio-hub .project-hub-tile-label,
#view-project-hub .project-hub-tile-label {
  color: rgba(255, 255, 255, 0.92);
  font-weight: 700;
  letter-spacing: 0.8px;
  transition: color 0.18s ease;
}
/* Short silver/white underline instead of the app-wide (black) --accent
   token, which would be invisible against this page's black background. */
#view-studio-hub .project-hub-tile-label::after,
#view-project-hub .project-hub-tile-label::after {
  background: rgba(255, 255, 255, 0.55);
  transition: width 0.18s ease, box-shadow 0.18s ease;
}
#view-studio-hub .project-hub-tile:hover .project-hub-tile-label,
#view-project-hub .project-hub-tile:hover .project-hub-tile-label { color: #ffffff; }
#view-studio-hub .project-hub-tile:hover .project-hub-tile-label::after,
#view-project-hub .project-hub-tile:hover .project-hub-tile-label::after {
  width: 30px;
  background: #ffffff;
  box-shadow: 0 0 8px rgba(255, 255, 255, 0.45);
}

@media (max-width: 720px) {
  #view-studio-hub .studio-frame,
  #view-project-hub .studio-frame { padding: 20px 0 8px; }
}

/* ---------- mobile "back to home" button ---------- */

.mobile-back-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-muted);
  font-family: var(--font-body);
  font-size: 0.78125rem;
  font-weight: 600;
  padding: 7px 12px;
  border-radius: 999px;
  cursor: pointer;
  margin-bottom: 12px;
}
.mobile-back-btn:hover, .mobile-back-btn:active {
  color: var(--text);
  border-color: var(--accent);
}

/* Storyboard's content column uses a narrower max-width (1180px) than the
   standard creative-page column (1600px, see #view-characters.active etc.),
   so centering it (margin-left/right: auto) leaves the Back button sitting
   210px ((1600-1180)/2) further right than on the other pages, even though
   the button markup/component is identical. Pull only the Back button back
   by that fixed offset so it lines up with the standard column's left edge
   — title/subtitle/Shot-Extractor-link/output/page width are all untouched.
   Scoped to the desktop layout (>1024px, above this file's 860px mobile
   breakpoint) since below that the column is no longer centered/constrained
   by max-width and the two pages' Back buttons already align without help. */
@media (min-width: 1024px) {
  #view-storyboard .mobile-back-btn { margin-left: -210px; }
}

/* ---------- settings (single model + key slot) ---------- */

.settings-single-row {
  display: flex; flex-direction: column; gap: 14px; max-width: 460px;
}
.settings-key-status {
  font-size: 0.75rem; color: var(--text-faint);
}
.settings-key-status.set { color: var(--success); }
.settings-single-actions { display: flex; gap: 8px; }

.node-generate-btn {
  position: absolute; right: 12px; bottom: 12px;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--accent); color: #ffffff; border: none; cursor: pointer;
  display: flex; align-items: center; justify-content: center; font-size: 0.875rem;
}
.node-generate-btn:hover { filter: brightness(1.08); }
.node-generate-btn:disabled { opacity: 0.5; cursor: not-allowed; }

.node-batch-stepper {
  position: absolute; left: 12px; bottom: 12px;
  display: flex; align-items: center; gap: 6px;
  background: var(--surface-3); border: 1px solid var(--border-soft); border-radius: 999px;
  padding: 4px 8px; font-family: var(--font-mono); font-size: 0.75rem;
}
.node-batch-stepper button {
  background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 0.8125rem; width: 14px;
}

.node-result {
  margin-top: 8px; border-radius: 6px; overflow: hidden; background: var(--bg);
  aspect-ratio: var(--frame-ratio, 16 / 9); display: flex; align-items: center; justify-content: center;
}
.node-result img, .node-result video { width: 100%; height: 100%; object-fit: cover; display: block; }
.node-result-empty { color: var(--text-faint); font-size: 1.25rem; }

.node-status-line { margin-top: 6px; font-family: var(--font-mono); font-size: 0.6875rem; color: var(--text-muted); min-height: 14px; }
.node-status-line.error { color: var(--error); }
.node-status-line.success { color: var(--success); }

.node-connect-line { pointer-events: none; }

/* ---------- gallery ---------- */

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

.gallery-item {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: border-color 0.12s ease;
}
.gallery-item:hover { border-color: var(--border-soft); border-color: #333a44; }

.gallery-item .thumb {
  width: 100%;
  height: 170px;
  object-fit: cover;
  background: var(--surface-2);
  display: block;
}

.gallery-item .meta { padding: 12px 14px; }

.gallery-item .prompt {
  font-size: 0.8125rem;
  color: var(--text);
  margin-bottom: 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gallery-item .tag {
  font-family: var(--font-mono);
  font-size: 0.65625rem;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.badge {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.65625rem;
  padding: 3px 9px;
  border-radius: 999px;
  margin-top: 8px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
}
.badge.completed { background: var(--success-soft); color: var(--success); }
.badge.failed { background: var(--error-soft); color: var(--error); }
.badge.running, .badge.queued { background: var(--accent-soft); color: var(--accent); }

.gallery-empty {
  color: var(--text-muted);
  font-size: 0.875rem;
  padding: 40px 0;
  text-align: center;
  border: 1px dashed var(--border);
  border-radius: var(--radius-lg);
}

/* ---------- settings ---------- */

.settings-panel { padding: 6px 22px; }

.settings-row {
  display: grid;
  grid-template-columns: 130px 1fr auto auto;
  align-items: center;
  gap: 12px;
  padding: 16px 0;
  border-bottom: 1px solid var(--border-soft);
}
.settings-row:last-child { border-bottom: none; }

.settings-row .provider-label {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.875rem;
}

.settings-row input {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}

/* Matches the app-wide 860px mobile/tablet breakpoint used everywhere else
   (see the Phase 7 responsive audit) — this row previously only collapsed
   below 640px, so the 641-860px band (small tablets, many phones in
   landscape) kept the rigid 4-column grid and could clip provider
   labels/buttons. */
@media (max-width: 860px) {
  .settings-row { grid-template-columns: 1fr; }
}

.hidden { display: none !important; }

/* ---------- Scene Builder: two-column layout ---------- */

.scene-breadcrumb {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--accent);
  text-transform: uppercase;
  margin-bottom: 6px;
}

/* Scene: same real-desktop-width + centering treatment as the other
   generation pages. Full-bleed brushed-metal + dot background: main
   itself becomes the background layer, spanning the true workspace
   edge-to-edge (sidebar to far right, top to bottom of the page) —
   independent of the centered 1680px content column, which is
   re-applied directly on #view-scene below so the actual content
   renders at exactly the same position/size as before. */
main:has(> #view-scene.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  /* Dots folded directly into main's own background (not a ::before
     pseudo) so the pattern tiles across the full scrollable height
     instead of only the first viewport — see #view-explore's fix above. */
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-scene.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1680px;
  margin-left: auto;
  margin-right: auto;
}

.scene-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(360px, 30%);
  gap: 26px;
  align-items: start;
}

/* Each numbered group (Visual style / Camera & composition / Lens / Scene
   elements) is one continuous block — not a separate floating card — so
   the left column reads as one workspace with clear section breaks. */
.scene-group + .scene-group {
  margin-top: 34px;
  padding-top: 32px;
  border-top: 1px solid var(--border-soft);
}
.scene-group-title {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 16px;
}
.scene-group .scene-section + .scene-section { margin-top: 18px; }

/* Angle / Shot size / Aspect ratio / Camera as one horizontal row of
   grouped columns instead of four stacked sections. */
.scene-row {
  display: flex;
  flex-wrap: wrap;
  gap: 20px 28px;
}
.scene-row .scene-section {
  flex: 1 1 200px;
  min-width: 170px;
}

.scene-col-side {
  position: sticky;
  top: 20px;
  display: flex;
  flex-direction: column;
  gap: 22px;
}

.scene-col-main { display: flex; flex-direction: column; gap: 24px; min-width: 0; }

.scene-section { display: flex; flex-direction: column; gap: 10px; }

.section-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--text-label);
}

.section-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.section-label-row-end { justify-content: flex-end; }

.section-hint { font-size: 0.71875rem; color: var(--text-faint); font-family: var(--font-mono); }

/* Sub-headings inside Camera & composition (Technical framing / Lens) —
   one level below the numbered scene-group-title, grouping the compact
   pill rows that live underneath the Angle/Shot Size visual selectors. */
.scene-subgroup + .scene-subgroup {
  margin-top: 28px;
  padding-top: 26px;
  border-top: 1px solid var(--border-soft);
}
.scene-subgroup-title {
  font-family: var(--font-mono);
  font-size: 0.65625rem;
  font-weight: 700;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 16px;
}
/* Extra breathing room between the Shot size visual selector and the
   Technical framing sub-group beneath it, and between Camera &
   composition's Technical framing sub-group and the standalone Lens
   section below — the page felt compressed here at compact spacing. */
.scene-visual-block-last { margin-bottom: 10px; }
.scene-subgroup { margin-top: 28px; }

/* Clear separation between the three visual-selector blocks (Scene Style /
   Angle / Shot Size) — each one is a substantial row of cards now, not a
   thin pill line, so it needs its own visible boundary rather than relying
   on the same small gap used for compact fields elsewhere in Scene. */
.scene-visual-block {
  padding-bottom: 22px;
  border-bottom: 1px solid var(--border-soft);
}
/* Scene Style is the only visual block in its group, immediately followed
   by the next scene-group's own top border — drawing both produced a
   double divider line. Drop this block's own border-bottom so there's
   exactly one line between Scene Style and Camera & composition. */
.scene-visual-block-solo { border-bottom: none; padding-bottom: 0; }
.scene-group .scene-section.scene-visual-block + .scene-section {
  margin-top: 22px;
}
/* The last visual block in its group (Shot size) still separates itself
   from the compact Aspect ratio / Camera row beneath it — see the
   increased .scene-visual-block-last margin above for the actual gap. */

/* ------------------------------------------------------------------
   Visual selector — Scene Style / Angle / Shot Size card rows.
   One contained horizontal-scroll strip with edge arrows; placeholder
   thumbnails today, real example images later (see SCENE_THUMBNAILS in
   app.js) — nothing here needs to change when those arrive.
   ------------------------------------------------------------------ */
.visual-selector {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0;
}
.visual-selector-track {
  display: flex;
  gap: 12px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-behavior: smooth;
  scrollbar-width: none;
  padding: 2px 2px 6px;
  flex: 1 1 auto;
  min-width: 0;
  cursor: grab;
}
.visual-selector-track.dragging { cursor: grabbing; scroll-behavior: auto; }
.visual-selector-track::-webkit-scrollbar { display: none; }

.visual-selector-arrow {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-muted);
  font-family: var(--font-body);
  font-size: 0.9375rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 2;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease, opacity 0.12s ease;
}
.visual-selector-arrow:hover { border-color: var(--accent); color: var(--text); }
.visual-selector-arrow-left { margin-right: 8px; }
.visual-selector-arrow-right { margin-left: 8px; }
/* Arrows are carousel chrome: they stay VISIBLE whenever the row is rendered,
   never tied to selection or hover. When the row can't scroll at all, both
   arrows fade to a subtle disabled state (they don't disappear); when it can,
   only the arrow at the current edge dims. The opacity transition (on
   .visual-selector-arrow) keeps the change smooth rather than abrupt. */
.visual-selector:not(.scrollable) .visual-selector-arrow { opacity: 0.35; pointer-events: none; }
.visual-selector.at-start .visual-selector-arrow-left { opacity: 0.3; pointer-events: none; }
.visual-selector.at-end .visual-selector-arrow-right { opacity: 0.3; pointer-events: none; }

.visual-card {
  flex: 0 0 auto;
  width: 150px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  padding: 6px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
  /* Reset the base .pill-toggle's pill/text styling — cards use their own. */
  color: var(--text-muted);
  font-family: var(--font-body);
}
.visual-card:hover { border-color: var(--accent); }
/* Compatibility-matrix disabled state (see updateSceneCompatibilityUI in
   app.js) — stays visible per the audit spec ("disabled options should
   remain visible but clearly unavailable"), never hidden or removed. */
.visual-card.disabled {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}
.visual-card.disabled:hover { border-color: var(--border); }
.visual-card.active {
  background: var(--surface-2);
  border-color: var(--accent);
  border-width: 2px;
  padding: 5px;
  box-shadow: 0 0 0 1px var(--accent);
  color: var(--text);
}
/* Small check indicator, in addition to the border — selection is never
   color-only. */
.visual-card.active .visual-card-thumb::after {
  content: '✓';
  position: absolute;
  top: 4px;
  right: 4px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  font-size: 0.625rem;
  line-height: 16px;
  text-align: center;
}

.visual-card-thumb {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: 5px;
  overflow: hidden;
  background: var(--surface);
}
.visual-card-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Neutral silver/graphite placeholder placard — communicates "example
   image will go here" without any instructional text, and doubles as the
   fallback if a real thumbnail URL ever fails to load. */
.visual-card-thumb-placeholder {
  background: linear-gradient(135deg, var(--surface-3, #d8d8d8) 0%, var(--surface-2) 45%, var(--surface-3, #d8d8d8) 100%);
}
.visual-card-thumb-placeholder::before {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 22px;
  height: 22px;
  opacity: 0.35;
  background: currentColor;
  color: var(--text-faint);
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M21 19V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z'/></svg>") center / contain no-repeat;
  mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M21 19V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z'/></svg>") center / contain no-repeat;
}
.visual-card-label {
  font-size: 0.8125rem;
  font-weight: 600;
  text-align: center;
  line-height: 1.25;
  color: inherit;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 900px) {
  .visual-card { width: 126px; }
  .visual-selector-arrow { width: 24px; height: 24px; font-size: 0.8125rem; }
}
@media (max-width: 600px) {
  .visual-card { width: 114px; }
  .visual-card-label { font-size: 0.75rem; }
}

/* background: prompt / upload tabs */
.scene-bg-pane[hidden] { display: none; }

/* Scene elements — Background / Color grading / Characters / Elements /
   Scene detailing are consolidated into one bordered panel (below) instead
   of each living in its own boxed card; thin dividers between
   .scene-elements-item children take the place of the separate borders. */
.scene-elements-panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.scene-elements-item { display: flex; flex-direction: column; gap: 10px; padding: 22px 20px; }
.scene-elements-item + .scene-elements-item { border-top: 1px solid var(--border-soft); }

/* color grading row */
.scene-colorgrade-row {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
.scene-colorgrade-desc { font-size: 0.78125rem; color: var(--text-faint); margin-top: 4px; }

/* character / element upload boxes — wide, shallow zones instead of a
   tall square tile grid, so this section reads as a compact strip. Now
   plain sections inside .scene-elements-panel rather than their own
   bordered cards. */
.scene-upload-pair { display: flex; flex-direction: column; gap: 12px; }
.scene-upload-box-head {
  display: flex; align-items: baseline; justify-content: space-between;
  font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 700;
  letter-spacing: 0.6px; text-transform: uppercase; color: var(--text-label);
  margin-bottom: 10px;
}
.scene-upload-box-head span:last-child { color: var(--text-muted); text-transform: none; letter-spacing: 0; }
.scene-upload-grid {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  gap: 8px;
}
.scene-upload-tile {
  position: relative;
  flex: 0 0 96px;
  aspect-ratio: 16/9;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px;
  color: var(--text-faint);
  font-size: 0.65625rem;
  font-family: var(--font-mono);
  cursor: pointer;
  overflow: hidden;
}
.scene-upload-tile:hover { border-color: var(--accent); color: var(--text-muted); }
.scene-upload-tile .upload-icon { font-size: 0.9375rem; }
.scene-upload-tile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.scene-upload-tile-remove {
  position: absolute; top: 3px; right: 3px; width: 16px; height: 16px; border-radius: 50%;
  background: rgba(0,0,0,0.65); color: #fff; border: none; font-size: 0.625rem;
  display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 2;
}
.scene-upload-box .lib-pick-btn { margin-top: 10px; }

/* Background upload control (Scene → Scene Elements). Reuses the exact
   .scene-upload-tile component Characters/Elements already use — a compact
   96px/16:9 dashed tile, NOT a full-width bar — instead of introducing a
   second upload-affordance style. .scene-upload-tile normally lives inside
   a flex .scene-upload-grid, which is what gives it its 96px width via
   flex-basis; Background is a single standalone tile with no grid parent,
   so this sets that same width explicitly and keeps it block-level (not
   full-width) so the full-width "Choose from library" button underneath
   reads as the secondary action, same as Characters/Elements. */
.scene-bg-upload-tile {
  display: flex;
  width: 96px;
  margin-bottom: 8px;
}
/* Selected-image preview: same thumbnail treatment as the Characters/
   Elements filled tiles (background-image + cover, icon/label hidden,
   circular ✕ remove button top-right) so Background's preview is visually
   identical regardless of whether the image came from local upload, drag/
   drop, or "Choose from library" — all three paths funnel through the same
   setFileDropThumb() call against this one tile. */
.scene-bg-upload-tile.has-image {
  background-size: cover;
  background-position: center;
  border-style: solid;
}
.scene-bg-upload-tile.has-image span { display: none; }

/* Scene Elements "Choose from library" consistency fix (Background,
   Characters, and any future Elements library button). Both buttons already
   share the .lib-pick-btn class/style — height, radius, font, icon sizing,
   border, hover/pressed/disabled states are already identical — but they
   sat at different DOM depths: Characters' button is a direct child of
   .scene-elements-item, which is `display:flex; flex-direction:column` and
   so stretches direct block children to full width by default, while
   Background's button is nested one level deeper inside
   .scene-bg-upload-pane (not itself a flex container) and so kept its
   natural, text-sized width instead. That depth difference — not the
   shared button style — is what made Background look compact and
   Characters look stretched. Forcing width:100% here for every
   .lib-pick-btn inside Scene Elements makes both fill their own
   section/container width the same way, regardless of nesting depth, without
   changing .lib-pick-btn's appearance or behavior anywhere else in the app. */
.scene-elements-panel .lib-pick-btn {
  display: block;
  width: 100%;
  box-sizing: border-box;
  text-align: center;
}

/* live prompt panel */
.live-prompt-panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 14px;
}
.live-prompt-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.live-prompt-actions { display: flex; gap: 6px; }
.live-prompt-actions .text-btn {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted);
  font-family: var(--font-mono); font-size: 0.65625rem; font-weight: 700; letter-spacing: 0.4px;
  padding: 5px 10px; border-radius: 999px; cursor: pointer;
}
.live-prompt-actions .text-btn:hover { border-color: var(--accent); color: var(--text); }
#scene-live-prompt { border: none; background: none; padding: 0; resize: vertical; font-size: 0.78125rem; }
#scene-live-prompt:focus { box-shadow: none; }

/* ✦ Enhance prompt — one shared look for every generation page's prompt
   field (Image, Video, Character, Character Sheet, Scene, Storyboard, Shot
   Extractor, Workflow nodes). Sits directly under/beside its prompt field,
   never reorganizing the surrounding layout. */
.enhance-row {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 8px;
}
.enhance-btn {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.3px;
  padding: 6px 12px;
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.enhance-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.enhance-btn:disabled { cursor: default; opacity: 0.7; }
.enhance-undo-btn {
  background: none;
  border: none;
  color: var(--text-faint);
  font-size: 0.6875rem;
  text-decoration: underline;
  cursor: pointer;
  padding: 0;
}
.enhance-undo-btn:hover { color: var(--text-muted); }
/* Character page's Enhance sits inline in the single-line prompt row
   rather than on its own row beneath — the field is an <input>, not a
   textarea, so there's no room below it the way there is elsewhere. */
.generate-row .enhance-btn { flex-shrink: 0; }
.generate-row .enhance-row { margin-top: 0; justify-content: flex-start; }

/* generate row — stacked (count above button) rather than side-by-side, so
   the Generate button's own width isn't eaten by the count label and can
   span edge-to-edge, matching the preview box directly beneath it. */
.scene-generate-row { display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
.scene-outputs-count {
  font-family: var(--font-mono); font-size: 0.71875rem; color: var(--text-faint);
  white-space: nowrap;
}
.scene-generate-row .primary-btn { margin-top: 0; width: 100%; }
.scene-generate-badges { display: flex; gap: 10px; margin-top: -4px; }
.scene-badge {
  font-family: var(--font-mono); font-size: 0.6875rem; color: var(--text-faint);
}

.scene-output-box { min-height: 260px; border-radius: var(--radius-lg); position: relative; }

/* Same near-invisible dotted-grid canvas texture used on Image/Video/
   Characters/Character Sheet — tiny low-contrast dots, brighter at
   center, fading toward the edges, sitting behind the generated frame
   so it only shows in unused canvas space. */
.scene-output-box::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: radial-gradient(rgba(255, 255, 255, 0.09) 1px, transparent 1px);
  background-size: 20px 20px;
  -webkit-mask-image: radial-gradient(ellipse at center, black 0%, rgba(0, 0, 0, 0.55) 45%, transparent 85%);
  mask-image: radial-gradient(ellipse at center, black 0%, rgba(0, 0, 0, 0.55) 45%, transparent 85%);
  pointer-events: none;
  z-index: 0;
}
.scene-output-box > * { position: relative; z-index: 1; }

/* scene history — compact cards matching the Video/Character/Charsheet
   History treatment (.entity-card inside a .card-grid): fixed-height
   16:9-ish cover thumbnail, prompt excerpt + meta below, actions row.
   scene-history-grid is just .card-grid with no overrides needed for
   sizing; these rules only add the scene-specific prompt clamp and the
   compact Reuse button. */
.scene-history-card .scene-history-prompt {
  font-style: italic; color: var(--text);
  font-family: var(--font-body); font-size: 0.78125rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  margin-bottom: 4px; display: block;
}
.scene-history-actions { flex-wrap: wrap; gap: 6px; }
.scene-history-reuse {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted);
  font-family: var(--font-body); font-size: 0.6875rem; font-weight: 600;
  padding: 5px 10px; border-radius: var(--radius); cursor: pointer;
}
.scene-history-reuse:hover { border-color: var(--accent); color: var(--text); }

@media (max-width: 1080px) {
  .scene-layout { grid-template-columns: minmax(0, 1fr); }
  .scene-col-side { position: static; }
}

/* Mobile-only: pull the Scene output up so it lands right after "4. Scene
   elements" instead of after the whole right-hand rail (Model / Moodboard /
   Batch / Live prompt / Generate). Desktop is untouched — .scene-layout
   stays a two-column grid above 1080px and these rules never apply there.
   Technique: flatten .scene-col-main/.scene-col-side with display:contents
   so their children become direct items of the still-single-column
   .scene-layout grid, then use `order` to interleave them — same pattern
   already used for .video-console-bottom on mobile. No DOM move, no
   duplicated viewer: it's the same #scene-result-area, just reordered. */
@media (max-width: 1080px) {
  #view-scene .scene-col-main,
  #view-scene .scene-col-side { display: contents; }

  #view-scene .scene-col-main > .scene-group:nth-of-type(1) { order: 1; } /* 1. Visual style */
  #view-scene .scene-col-main > .scene-group:nth-of-type(2) { order: 2; } /* 2. Camera & composition */
  #view-scene .scene-col-main > .scene-group:nth-of-type(3) { order: 3; } /* 3. Lens */
  #view-scene .scene-col-main > .scene-group:nth-of-type(4) { order: 4; } /* 4. Scene elements */

  #view-scene .scene-col-side > .scene-section:has(#scene-result-area) { order: 5; } /* Scene output */

  #view-scene .scene-col-side > .scene-section:nth-child(1) { order: 6; } /* Model */
  #view-scene .scene-col-side > .scene-section:nth-child(2) { order: 7; } /* Moodboard */
  #view-scene .scene-col-side > .scene-section:nth-child(3) { order: 8; } /* Batch size */
  #view-scene .scene-col-side > .live-prompt-panel { order: 9; }
  #view-scene .scene-col-side > div:nth-child(5) { order: 10; } /* Generate button/progress/status */
}

/* ---------- final mobile refinements (kept last so they win the cascade) ---------- */

@media (max-width: 640px) {
  main { padding: 20px 14px 40px; }
  .field-row { grid-template-columns: 1fr; gap: 16px; }
  .dash-heading h1 { font-size: 1.625rem; }
  .hero-card { min-height: 190px; padding: 20px; }
  .hero-card h2 { font-size: 1.25rem; }
  .panel { padding: 16px; }
  .console-grid { gap: 16px; }
  .viewfinder-panel { min-height: 300px; }
  .viewfinder-frame { max-height: 70vh; }
  .viewfinder-frame video { max-height: 70vh; }
  h1 { font-size: 1.3125rem; }
  .card-grid { grid-template-columns: repeat(4, 1fr); gap: 12px; }
  .quick-tools-row { grid-template-columns: repeat(2, 1fr); }
  .node-select-row { flex-direction: column; }
  .scene-upload-pair { grid-template-columns: 1fr 1fr; gap: 10px; }
  .scene-generate-row { flex-direction: column; align-items: stretch; }
  .video-console-grid { grid-template-columns: minmax(0, 1fr); }
  .panel-head-row { flex-direction: column; align-items: flex-start; gap: 12px; }
  .field-pair { grid-template-columns: 1fr; }
}

/* ---------- video: generate video redesign ---------- */

.video-top-tabs { flex: 0 0 auto; }

/* The video module needs real desktop width — let it break out of the
   normal 1180px reading column (same technique used for the workflow
   canvas and the home hero) so the output preview and the control panel
   below it can both stretch across the available viewport instead of
   being boxed into the left half of the screen. */
/* Full-bleed brushed-metal + dot background: main itself becomes the
   background layer, spanning the true workspace edge-to-edge (sidebar to
   far right, top to bottom of the page) — independent of the centered
   1680px content column, which is re-applied directly on #view-video
   below so the actual content renders at exactly the same position/size
   as before. Same technique used for Projects/Image. */
main:has(> #view-video.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  /* Dots folded directly into main's own background (not a ::before
     pseudo) so the pattern tiles across the full scrollable height
     instead of only the first viewport — see #view-explore's fix above. */
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-video.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1680px;
  margin-left: auto;
  margin-right: auto;
}

/* Video console: output preview sits full-width on its own row up top —
   nothing ever shares that row with it, so nothing can ever shrink or
   displace it — and is capped to a 16:9-ish proportion so it doesn't get
   excessively tall as the panel widens. Everything else (generation mode,
   prompt, settings, reference images, elements) lives in ONE continuous
   wide panel below, laid out as internal columns rather than two separate
   cards with a gap between them. */
.video-console-grid {
  display: flex;
  flex-direction: column;
  gap: 22px;
}
.video-console-grid .viewfinder-panel {
  width: 100%;
  min-height: 340px;
}
.video-console-grid .viewfinder-panel .viewfinder {
  flex: none;
  aspect-ratio: 16 / 9;
  max-height: 62vh;
  position: relative;
}
/* A near-invisible dotted-grid texture across the empty canvas — tiny,
   low-contrast dots, brighter toward the center and fading out toward the
   edges via a mask, so it reads as a creative-workspace surface rather
   than a flat black rectangle. Sits behind the actual frame/video content
   (z-index below), so once a result is showing it's only ever visible in
   the unused margin around it. */
.video-console-grid .viewfinder-panel .viewfinder::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: radial-gradient(rgba(255, 255, 255, 0.09) 1px, transparent 1px);
  background-size: 20px 20px;
  -webkit-mask-image: radial-gradient(ellipse at center, black 0%, rgba(0, 0, 0, 0.55) 45%, transparent 85%);
  mask-image: radial-gradient(ellipse at center, black 0%, rgba(0, 0, 0, 0.55) 45%, transparent 85%);
  pointer-events: none;
  z-index: 0;
}
.video-console-grid .viewfinder-panel .viewfinder > * {
  position: relative;
  z-index: 1;
}
.video-console-bottom {
  /* This is now the single merged panel — control-panel and flyout-panel
     lose their own .panel styling below and become plain inner columns.
     align-items: stretch (the default) keeps both columns the exact same
     height as each other — same outer container size, same divider length —
     regardless of how much content either side has; the shorter side's
     leftover space is distributed within it (see .control-panel below)
     rather than left as a gap after the shorter column's content ends. */
  background: var(--panel-sheen), var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md), var(--inset-hairline);
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  align-items: stretch;
}
.video-console-bottom .control-panel,
.video-console-bottom .flyout-panel {
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 0;
  padding: 26px;
  min-width: 0;
  max-width: none;
  display: block;
}
.video-console-bottom .control-panel {
  /* Fewer fields live here now (Audio/Video reference removed) than in the
     right-hand column — flex-column lets the Prompt field (below) grow to
     absorb that leftover vertical space itself, so the panel's content
     fills it naturally instead of leaving a dead gap. This only
     redistributes existing spacing; it never changes the panel's width or
     the container's overall height. */
  display: flex;
  flex-direction: column;
}
.video-console-bottom .flyout-panel {
  border-left: 1px solid var(--border-soft);
}
@media (max-width: 1100px) {
  .video-console-bottom { grid-template-columns: minmax(0, 1fr); }
  .video-console-bottom .flyout-panel {
    border-left: none;
    border-top: 1px solid var(--border-soft);
  }
}

/* Mobile: put Generate as the very last control, right before History.
   The button lives inside .control-panel in the markup (so desktop keeps
   its current position at the bottom of the left column), but on mobile
   .control-panel is flattened via display:contents so its fields become
   direct items of the .video-console-bottom stack alongside .flyout-panel,
   letting `order` place the flyout-panel's frame/reference/motion fields
   ahead of the generate cluster without moving anything in the HTML. */
@media (max-width: 860px) {
  .video-console-bottom .control-panel { display: contents; }
  .video-console-bottom .control-panel > .field,
  .video-console-bottom .control-panel > .field-pair {
    padding-left: 26px;
    padding-right: 26px;
  }
  .video-console-bottom .control-panel > .field:first-child { padding-top: 26px; }
  .video-console-bottom .flyout-panel { order: 1; }
  .video-console-bottom .control-panel > .video-generate-cluster {
    order: 2;
    padding: 0 26px 26px;
  }
}

/* The Prompt field is the one field that should grow: it absorbs whatever
   leftover vertical space the flex-column control panel has (from the
   removed Audio/Video reference fields), so a longer Prompt box — not a
   blank gap right above Generate — is what makes the left panel reach the
   same height as the right-hand Elements column. Duration/Aspect
   Ratio/Resolution/Seed/Model/Generate Audio/Batch Size and the Generate
   button itself keep their normal intrinsic size and spacing; only this
   one field flexes. min-height: 0 is required on a flex child that
   contains a scrollable/growable box like a textarea, or it refuses to
   shrink below its content's default intrinsic height and the flex-grow
   has nothing to visibly do. */
.video-console-bottom .control-panel #video-prompt-field {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
.video-console-bottom .control-panel #video-prompt-field .tag-textarea-wrap {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.video-console-bottom .control-panel #video-prompt-field textarea#video-prompt {
  flex: 1 1 auto;
  height: auto;
  min-height: 110px;
}

.segmented.seg-3 .seg-btn,
.segmented.seg-2 .seg-btn,
.segmented.seg-4 .seg-btn { flex: 1; text-align: center; }
.segmented.seg-3, .segmented.seg-2, .segmented.seg-4, .segmented.seg-5 { display: flex; width: 100%; }
.segmented.seg-5 .seg-btn { flex: 1; text-align: center; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Compact variant — used where a segmented control should read as a small
   secondary mode switch rather than a primary full-width action bar (e.g.
   Enhance's Image/Video media-type switch, sitting above the larger
   Upscale/Remaster control). Only wide enough for its two labels. */
.segmented-compact,
.segmented-compact.seg-2,
.segmented-compact.seg-3,
.segmented-compact.seg-4 {
  display: inline-flex;
  width: auto;
}
.segmented-compact .seg-btn {
  flex: none;
  padding: 5px 13px;
  font-size: 0.6875rem;
  min-width: 0;
}

/* Wrapper for the media-type switch — small muted label + compact
   segmented control, deliberately lightweight next to the page
   description above and the larger Upscale/Remaster control below. */
.media-type-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 4px 0 18px;
}
.media-type-label {
  font-family: var(--font-mono);
  font-size: 0.65625rem;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--text-faint);
}
@media (max-width: 640px) {
  .media-type-row { margin-bottom: 16px; }
}

/* Divider between the flyout panel's stacked field sections (Prompt Tips /
   First & Last Frame / Reference / Motion Control) — only between sections
   that are ACTUALLY visible right now (Text to Video hides Frames+Reference
   entirely, Image to Video may hide Reference alone, etc. — see
   getVideoVisibleInputs in app.js). The general sibling combinator (~),
   not the adjacent one (+), is deliberate: it only requires that SOME
   earlier field be visible, not that the IMMEDIATELY preceding one is —
   so a hidden field sitting between two visible ones never leaves a
   floating divider with nothing visible above it, and removing a section
   never leaves the next visible one still carrying a stray border-top from
   a sibling that no longer renders. */
.flyout-panel .field:not(.hidden) ~ .field:not(.hidden) {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--border-soft);
}
.seg-btn[disabled] { opacity: 0.4; cursor: not-allowed; }

/* Edit Video: the right column holds only the source clip and the optional
   keyframes, so the two drop zones share the column's height between them
   (source clip first, since it is the required one) instead of leaving the
   bottom of the column empty. Scoped to the mode class the tab handler sets. */
/* Motion Control tab (motionReferenceMode): unavailable card, the camera
   behavior radios and the "use reference for" switches. Same tokens as the
   surrounding slots; nothing here changes the page's layout. */
.motion-unavailable {
  border: 1px dashed var(--border); border-radius: var(--radius); padding: 16px; margin-top: 4px;
}
.motion-unavailable-title {
  font-family: var(--font-mono); font-size: 0.71875rem; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase; color: var(--text-label);
}
.mr-choice {
  display: flex; align-items: flex-start; gap: 10px; padding: 9px 10px; margin-top: 6px; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-2);
  transition: border-color 0.12s ease;
}
.mr-choice:hover { border-color: var(--accent); }
.mr-choice input { margin: 3px 0 0; flex-shrink: 0; accent-color: var(--accent); }
.mr-choice span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.mr-choice strong { font-size: 0.78125rem; font-weight: 600; color: var(--text); }
.mr-choice small { font-size: 0.71875rem; color: var(--text-muted); line-height: 1.4; }
.mr-choice:has(input:checked) { border-color: var(--accent); }
.mr-check-list { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px; margin-top: 6px; }
.mr-check { display: flex; align-items: center; gap: 8px; font-size: 0.78125rem; color: var(--text); cursor: pointer; min-width: 0; }
.mr-check input { accent-color: var(--accent); flex-shrink: 0; }
.mr-check.is-off { color: var(--text-muted); }
@media (max-width: 560px) { .mr-check-list { grid-template-columns: 1fr; } }

#video-flyout-panel.edit-mode { display: flex; flex-direction: column; }
#video-flyout-panel.edit-mode .flyout-inner { display: flex; flex-direction: column; flex: 1; min-height: 0; }
#video-flyout-panel.edit-mode #video-elements-field { flex: 1; display: flex; flex-direction: column; min-height: 0; }
#video-flyout-panel.edit-mode #video-elements-videos-slot { order: 1; }
#video-flyout-panel.edit-mode #video-elements-images-slot { order: 2; }
#video-flyout-panel.edit-mode #video-elements-videos-slot,
#video-flyout-panel.edit-mode #video-elements-images-slot {
  flex: 1; display: flex; flex-direction: column; min-height: 0; margin-bottom: 0;
}
#video-flyout-panel.edit-mode #video-elements-videos-slot { margin-bottom: 22px; }
#video-flyout-panel.edit-mode #video-elements-videos-slot .elements-dropzone,
#video-flyout-panel.edit-mode #video-elements-images-slot .elements-dropzone {
  flex: 1; min-height: 120px; display: flex; align-items: center; justify-content: center; font-size: 0.8125rem;
}
#video-flyout-panel.edit-mode #video-elements-images-slot .lib-pick-btn { margin-top: 10px; }

.field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
.field-pair .field { margin-bottom: 0; }

.text-input {
  width: 100%; background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text); border-radius: var(--radius); padding: 10px 12px;
  font-family: var(--font-mono); font-size: 0.8125rem;
}

.row-toggle-field { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.row-toggle-field label { font-weight: 600; }
.row-toggle-field .section-hint { margin-top: 2px; }
/* "Generate audio" is a heading like Duration/Aspect Ratio/Resolution/Model/
   Batch Size above it, so it uses .section-label for the same soft-white
   color, weight, uppercase treatment, letter spacing, and size — but this
   row's own label/font-weight rules (just above) are otherwise more specific
   than the shared .section-label rule, so re-assert weight here, and zero
   out .section-label's normal bottom margin so the helper text underneath
   keeps its existing spacing instead of gaining extra gap. */
.row-toggle-field label.section-label { font-weight: 700; margin-bottom: 0; }

.tag-textarea-wrap { position: relative; max-width: 100%; min-width: 0; box-sizing: border-box; }
.tag-autocomplete {
  position: absolute; left: 0; right: 0; bottom: calc(100% + 4px);
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius);
  max-height: 160px; overflow-y: auto; z-index: 5;
}
.tag-autocomplete-item {
  padding: 6px 12px; font-family: var(--font-mono); font-size: 0.78125rem; cursor: pointer; color: var(--text-muted);
  display: flex; align-items: center; gap: 8px;
}
.tag-autocomplete-item:hover, .tag-autocomplete-item.active { background: var(--surface); color: var(--text); }
/* Compact 24px visual-confirmation thumbnail beside each @tag row — kept
   small and subtle by design (NOT the Library's larger card thumbnails).
   Image/video references reuse the same square cover-cropped treatment;
   audio has no visual frame, so it gets a tiny waveform glyph instead of an
   invented thumbnail; a reference with no resolvable url yet renders an
   empty placeholder square rather than a broken image icon. */
.tag-autocomplete-thumb {
  width: 24px; height: 24px; border-radius: 4px; object-fit: cover; flex: none;
  background: var(--surface); border: 1px solid var(--border);
}
.tag-autocomplete-thumb-audio, .tag-autocomplete-thumb-empty {
  display: flex; align-items: center; justify-content: center;
  font-size: 0.75rem; color: var(--text-muted);
}

.frame-dropzone-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.frame-dropzone {
  position: relative; border: 1px dashed var(--border); border-radius: var(--radius-lg);
  background: var(--surface-2); aspect-ratio: 1/1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 6px; text-align: center; padding: 14px;
  cursor: pointer; transition: border-color 0.12s ease, color 0.12s ease;
}
.frame-dropzone:hover { border-color: var(--accent); }
.frame-dropzone input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.frame-dropzone-icon { font-size: 1.375rem; color: var(--text-faint); }
.frame-dropzone-label { font-size: 0.8125rem; color: var(--text); font-weight: 600; }
.frame-dropzone-label .label-note { color: var(--text-faint); font-weight: 400; }
.frame-dropzone-hint { font-family: var(--font-mono); font-size: 0.71875rem; color: var(--text-faint); }

.frame-dropzone-preview {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  border-radius: var(--radius-lg); display: none;
}
.frame-dropzone.has-image .frame-dropzone-preview { display: block; }
.frame-dropzone.has-image .frame-dropzone-icon,
.frame-dropzone.has-image .frame-dropzone-label { display: none; }
.frame-dropzone.has-image .frame-dropzone-hint {
  position: absolute; left: 0; right: 0; bottom: 0; margin: 0; padding: 6px 8px;
  background: linear-gradient(to top, rgba(0,0,0,0.75), transparent);
  color: #fff; text-align: left; border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.frame-dropzone.has-image:hover::after {
  content: '✕'; position: absolute; top: 6px; right: 6px; width: 22px; height: 22px;
  border-radius: 50%; background: rgba(0,0,0,0.65); color: #fff; font-size: 0.75rem;
  display: flex; align-items: center; justify-content: center; pointer-events: none;
}

#charsheet-clothing-drop.has-image {
  background-size: cover; background-position: center; color: transparent; min-height: 90px;
}
#charsheet-clothing-drop.has-image span { display: none; }

.elements-slot { margin-bottom: 16px; }
.elements-slot-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-family: var(--font-mono); font-size: 0.71875rem; font-weight: 700; letter-spacing: 0.6px;
  color: var(--text-label); margin-bottom: 4px;
}
.elements-slot-head span:last-child { color: var(--text-faint); }
.elements-dropzone {
  position: relative; border: 1px dashed var(--border); border-radius: var(--radius);
  padding: 16px; text-align: center; font-size: 0.78125rem; color: var(--text-faint); margin-top: 6px;
  cursor: pointer; transition: border-color 0.12s ease, color 0.12s ease;
}
.elements-dropzone:hover { border-color: var(--accent); color: var(--text-muted); }
.elements-dropzone input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
/* Reached the selected model's real reference limit (or the model has no
   Reference capability at all) — see updateVideoReferenceUI() in app.js.
   Visually inert rather than removed, so the count/limit stays visible. */
.elements-dropzone.is-disabled { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
.elements-dropzone.is-disabled input[type="file"] { cursor: not-allowed; }

/* Shared drag-and-drop / paste-to-upload highlight — applied to whichever
   upload zone (dropzone, file-drop, or a grid of upload tiles) the file is
   being dragged over or is the active paste target. */
.elements-dropzone.drop-active,
.file-drop.drop-active,
.scene-upload-grid.drop-active,
.scene-bg-upload-tile.drop-active {
  border: 1px dashed var(--accent) !important;
  background: var(--accent-soft) !important;
  color: var(--text) !important;
  outline: 2px dashed var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius);
}
/* Same drag highlight for a grid of upload tiles (Moodboard editor) or an
   upload tile itself (Moodboard "choose from library" picker) — kept in the
   same monochrome family as the rule above rather than duplicating it. */
.moodboard-editor-grid.drop-active {
  outline: 2px dashed var(--accent);
  outline-offset: 2px;
  background: var(--accent-soft);
  border-radius: var(--radius);
}
.elements-upload-tile.drop-active .elements-card-thumb {
  border-color: var(--accent); color: var(--accent); background: var(--accent-soft);
}
.elements-dropzone-row { display: flex; align-items: flex-start; gap: 8px; }
.elements-dropzone-row .elements-dropzone { flex: 1; margin-top: 6px; }
.dropzone-clear-btn {
  flex-shrink: 0; margin-top: 6px; width: 28px; height: 28px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-muted);
  font-size: 0.8125rem; cursor: pointer; transition: border-color 0.12s ease, color 0.12s ease;
}
.dropzone-clear-btn:hover { border-color: var(--danger, #e5484d); color: var(--danger, #e5484d); }
.dropzone-clear-btn.hidden { display: none; }
.elements-thumb-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.elements-thumb {
  width: 56px; height: 56px; border-radius: 8px; border: 1px solid var(--border);
  background: var(--surface-2) center/cover no-repeat; position: relative;
  display: flex; align-items: flex-end; justify-content: center;
}
.elements-thumb-tag {
  font-family: var(--font-mono); font-size: 0.5625rem; font-weight: 700; color: #fff;
  background: rgba(0,0,0,0.65); border-radius: 4px; padding: 1px 4px; margin: 0 0 3px;
}
.elements-thumb-audio, .elements-thumb-video {
  display: flex; align-items: center; justify-content: center; font-size: 1.125rem; color: var(--text-faint);
}
.elements-thumb-remove {
  position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted);
  font-size: 0.6875rem; line-height: 14px; text-align: center; cursor: pointer;
}
.elements-thumb-remove:hover { color: #fff; background: var(--danger, #d9534f); border-color: transparent; }

/* Motion Control's Motion Reference slot — a real <video> preview inside the
   same .elements-thumb frame the image slots use (renderMotionControlSlot()
   in app.js), rather than a background-image thumb, since a video reference
   has no single frame to show as a static thumbnail. */
.elements-thumb-video-preview { overflow: hidden; padding: 0; }
.elements-thumb-video-preview video { width: 100%; height: 100%; object-fit: cover; display: block; }

.workflow-save-row { display: flex; align-items: center; gap: 10px; }
.workflow-save-row .status-line { font-size: 0.6875rem; }

.wf-hist-btn {
  width: 28px; height: 28px; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text-muted); font-size: 0.875rem; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.wf-hist-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.wf-hist-btn:disabled { opacity: 0.35; cursor: default; }

/* ---------- Quick Add toolbar ---------- */
.wf-quickadd-row {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 4px 10px;
  flex-wrap: wrap;
}
.wf-quickadd-btn {
  display: flex; align-items: center; gap: 6px;
  background: var(--surface-1, #17130f); border: 1px solid var(--border-soft);
  color: var(--text-muted); font-size: 0.75rem; font-weight: 600;
  padding: 6px 12px; border-radius: 999px; cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}
.wf-quickadd-btn .wf-qa-icon { font-size: 0.75rem; color: var(--text-faint); transition: color 0.15s; }
.wf-quickadd-btn:hover { border-color: var(--accent); color: var(--text); }
.wf-quickadd-btn:hover .wf-qa-icon { color: var(--accent); }

/* ---------- node count pill ---------- */
.node-count-pill {
  position: absolute; left: 16px; bottom: 16px; z-index: 24;
  background: var(--surface-2); border: 1px solid var(--border-soft);
  color: var(--text-faint); font-size: 0.6875rem; font-weight: 500;
  padding: 6px 12px; border-radius: 999px;
  pointer-events: none; opacity: 0.85;
}
.node-count-pill.hidden { display: none; }
@media (max-width: 860px) {
  .node-count-pill { display: none; }
}

/* ---------- minimap ---------- */
.node-minimap {
  position: absolute; right: 16px; bottom: 66px; z-index: 24;
  width: 160px; height: 110px; border-radius: 10px;
  background: rgba(10, 8, 6, 0.75); border: 1px solid var(--border-soft);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
  overflow: hidden;
}
.node-minimap canvas { display: block; width: 100%; height: 100%; cursor: pointer; }
.node-minimap.hidden { display: none; }
@media (max-width: 860px) {
  .node-minimap { display: none; }
}

/* Credit cost, shown INSIDE the Generate button itself — one pill, icon +
   number, same background as the button, not a separate box next to it. */
.btn-credit {
  display: inline-flex; align-items: center; gap: 4px;
  margin-left: 8px; font-weight: 800; opacity: 0.85;
}
.btn-credit .btn-credit-icon { flex-shrink: 0; opacity: 0.9; }

/* ---------- @ Elements library ---------- */

.elements-trigger-btn {
  background: var(--surface-2); border: 1px solid var(--accent); color: var(--accent);
  font-family: var(--font-mono); font-size: 0.71875rem; font-weight: 700; letter-spacing: 0.3px;
  padding: 5px 12px; border-radius: 999px; cursor: pointer; transition: background 0.12s ease, color 0.12s ease;
}
.elements-trigger-btn:hover { background: var(--accent-soft); color: var(--accent); }

.elements-modal-card { max-width: 720px; }
.elements-modal-head-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px;
}
.elements-modal-tools { display: flex; align-items: center; gap: 8px; }
.elements-search-input { width: 200px; max-width: 100%; padding: 8px 12px; }

.elements-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 14px;
  max-height: 56vh; overflow-y: auto; padding-right: 2px;
}
/* Mobile: the grid becomes the ONE intended scroll owner inside the
   Elements Library / Elements Picker modal card — see the shared
   `.modal-card` flex-shell fix earlier in this file. A fixed `56vh` here
   would fight that flex sizing (and doesn't account for the dvh-based card
   height it now lives inside), so it's replaced with `flex:1 1 auto;
   min-height:0`, and the card's own scroll is turned off in favor of this
   one so there is never more than one vertical scroll container for the
   same content. */
@media (max-width: 860px) {
  .elements-modal-card {
    overflow-y: hidden;
  }
  .elements-modal-head-row {
    flex: 0 0 auto;
    flex-wrap: wrap;
  }
  .elements-search-input {
    width: 100%;
  }
  .elements-grid {
    max-height: none;
    flex: 1 1 auto;
    min-height: 0;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }
}
.elements-card {
  display: flex; flex-direction: column; cursor: pointer; text-align: left;
  background: none; border: none; padding: 0; font-family: inherit;
}
.elements-card-thumb {
  width: 100%; aspect-ratio: 1 / 1; border-radius: var(--radius-lg); border: 1px solid var(--border);
  background: var(--surface-2) center/cover no-repeat; overflow: hidden;
}
.elements-card-name {
  font-family: var(--font-mono); font-size: 0.78125rem; font-weight: 700; color: var(--accent);
  margin-top: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.elements-card-category {
  font-family: var(--font-mono); font-size: 0.6875rem; color: var(--text-faint); margin-top: 2px;
}
.elements-card:hover .elements-card-thumb { border-color: var(--accent); }

.elements-new-tile .elements-card-thumb {
  display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 6px;
  border: 1.5px dashed var(--border); background: var(--surface-2); color: var(--text-faint);
}
.elements-new-tile:hover .elements-card-thumb { border-color: var(--accent); color: var(--accent); }
.elements-new-tile .elements-new-plus { font-size: 1.625rem; line-height: 1; color: var(--accent); }
.elements-new-tile .elements-card-name { color: var(--text-muted); }

.elements-upload-tile {
  position: relative;
}
.elements-upload-tile .elements-card-thumb {
  display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 6px;
  border: 1.5px dashed var(--border); background: var(--surface-2); color: var(--text-faint);
}
.elements-upload-tile:hover .elements-card-thumb { border-color: var(--accent); color: var(--accent); }
.elements-upload-tile.dragover .elements-card-thumb { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.elements-upload-tile input[type="file"] {
  position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; z-index: 1;
}

.new-element-body-row { display: flex; gap: 18px; align-items: flex-start; }
.new-element-fields { flex: 1; min-width: 0; }
.new-element-preview {
  width: 120px; height: 120px; flex-shrink: 0; border-radius: var(--radius-lg);
  border: 1px solid var(--border); background: var(--surface-2); overflow: hidden;
}
.new-element-preview img { width: 100%; height: 100%; object-fit: cover; display: none; }

/* ---------- @-tag highlight overlay ---------- */

.tag-highlight-overlay {
  position: absolute; inset: 0; pointer-events: none; overflow: hidden;
  white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; z-index: 1;
  border: 1px solid transparent; border-radius: var(--radius);
  padding: 10px 12px; font-size: 0.84375rem; font-family: var(--font-body); line-height: 1.5;
  color: var(--text);
  box-sizing: border-box; max-width: 100%; min-width: 0;
}
.tag-highlight-overlay mark {
  /* Visually distinct @reference styling MUST NOT change glyph metrics.
     This overlay sits pixel-for-pixel on top of a real (but invisible —
     see .tag-textarea-wrap textarea below) textarea that owns the actual
     caret and renders every character at one uniform weight. font-weight
     (or any font-family/letter-spacing/font-size change) on just the
     <mark> span makes the token render wider here than in the textarea
     underneath, so every character typed after it drifts out of pixel
     alignment with the real caret — which is exactly what looked like the
     caret "jumping backward" while typing. color/text-shadow don't affect
     layout width, so they're safe; font-weight is not. */
  background: none; color: var(--accent); font: inherit;
  text-shadow: 0 0 0.35px currentColor, 0 0 0.35px currentColor;
}
.tag-textarea-wrap textarea {
  position: relative; z-index: 2; background: transparent !important; color: transparent !important;
  caret-color: var(--text);
  /* A textarea is inline-level by default, so its box sits on a text
     baseline and leaves room below it for descenders — a few extra pixels
     of blank space that the surrounding .tag-textarea-wrap (a block
     container sized to its in-flow child) inherits too. The overlay is
     absolutely positioned to fill that same wrap exactly, so it ends up a
     few pixels TALLER than the textarea's own visible border box — tall
     enough that the overlay's last line of text can render below the
     textarea's bottom border with nothing there to contain or fade it,
     which is what read as text "bleeding" past the box. display:block
     removes the inline baseline gap so the wrap (and therefore the
     overlay) is sized to exactly the textarea's real height. */
  display: block;
  /* The visible text is the .tag-highlight-overlay above; this textarea is
     the invisible layer that actually owns the caret and the value. Both
     layers MUST wrap identically or the overlay's rendered characters drift
     out of alignment with where the real (invisible) caret actually sits —
     which is what read as "the caret jumps backward" while typing/deleting,
     and as text "spilling" past the box when a long unbroken token (a long
     @reference, a URL) wrapped differently in one layer than the other.
     Two things caused the drift: (1) a native vertical scrollbar appearing
     on this textarea (but never on the overlay, which has no scrollbar)
     narrows this element's available width for wrapping while the overlay's
     stays full-width — hiding the scrollbar's own box (but not its scroll
     functionality) keeps both layers' wrap width identical at all times;
     (2) this element had no explicit wrapping/containment rules of its own,
     so a long unbroken token could overflow it while the overlay (which
     already had word-break) clipped or wrapped it differently. */
  box-sizing: border-box; width: 100%; max-width: 100%; min-width: 0;
  white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
  overflow-x: hidden; overflow-y: auto;
  scrollbar-width: none; /* Firefox: keep scrolling, hide the gutter */
  /* Once you scroll to the bottom of the overflowed content, a wheel
     gesture that keeps going must NOT hand off ("chain") to the page
     underneath — that's what made the box's own scroll limit look like the
     box's content spilling out/overlapping the controls below it. Keeping
     the scroll contained here is exactly acceptance-test 8's requirement:
     the page must never move because this box ran out of room to scroll. */
  overscroll-behavior: contain;
}
.tag-textarea-wrap textarea::-webkit-scrollbar { width: 0; height: 0; }
.tag-textarea-wrap textarea::placeholder { color: var(--text-faint); }
/* A hard clip at the exact pixel boundary can slice straight through the
   last visible line of text when content is scrolled — readable, contained,
   but visually reads as the text "running into" the border below it. Fading
   the final ~14% of the box instead makes a partial line look like an
   intentional edge-of-content cue rather than a glitch, without changing
   the box's actual dimensions or scroll behavior. */
.tag-highlight-overlay {
  -webkit-mask-image: linear-gradient(to bottom, #000 86%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 86%, transparent 100%);
}

/* ---------- @-tag highlighting on dynamic Workflow node prompts ---------- */
/* Same overlay technique as .tag-textarea-wrap, applied to textareas that
   are created on the fly per-node (see setupWorkflowPromptTagging() in app.js). */

.wf-prompt-tagwrap {
  position: relative; z-index: 2; background: transparent !important; color: transparent !important;
  caret-color: var(--text);
  /* display:block — see the matching comment on .tag-textarea-wrap textarea:
     a textarea's default inline-level box leaves baseline/descender space
     that can let an absolutely-positioned overlay sibling end up taller
     than this element's own visible border box. */
  display: block;
  /* Same invisible-textarea-plus-visible-overlay technique as .tag-textarea-wrap
     above (see that rule's comment for why every one of these lines matters) —
     kept in sync with it rather than drifting into a second implementation. */
  box-sizing: border-box; width: 100%; max-width: 100%; min-width: 0;
  white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
  overflow-x: hidden; overflow-y: auto;
  scrollbar-width: none;
  overscroll-behavior: contain;
}
.wf-prompt-tagwrap::-webkit-scrollbar { width: 0; height: 0; }
.wf-prompt-tagwrap-highlight {
  z-index: 1; overflow: hidden; box-sizing: border-box; max-width: 100%; min-width: 0;
  white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
}
.wf-tag-autocomplete {
  position: absolute; left: 12px; right: 12px; bottom: calc(100% + 4px); z-index: 6;
}

.node-box-prompt-tagwrap {
  background: transparent !important; color: transparent !important; caret-color: var(--text);
  z-index: 4;
  /* display:block — see the matching comment on .tag-textarea-wrap textarea:
     a textarea's default inline-level box leaves baseline/descender space
     that can let an absolutely-positioned overlay sibling end up taller
     than this element's own visible border box. */
  display: block;
  /* Same invisible-textarea-plus-visible-overlay technique as .tag-textarea-wrap
     above — see that rule's comment. Hiding this element's own scrollbar and
     matching its wrap rules to the overlay below keeps the two layers'
     wrapping identical so the overlay's visible text never drifts out of
     alignment with the real, invisible caret. */
  box-sizing: border-box; width: 100%; max-width: 100%; min-width: 0;
  white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
  overflow-x: hidden; overflow-y: auto;
  scrollbar-width: none;
  overscroll-behavior: contain;
}
.node-box-prompt-tagwrap::-webkit-scrollbar { width: 0; height: 0; }
.node-box-prompt-tagwrap-highlight {
  position: absolute; left: 0; right: 0; top: 0;
  min-height: 34%; max-height: 70%;
  background: linear-gradient(to bottom, rgba(10, 10, 12, 0.85) 0%, rgba(10, 10, 12, 0.5) 65%, transparent 100%);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: 10px 12px; font-size: 0.75rem; font-family: var(--font-body); line-height: 1.4;
  z-index: 3; pointer-events: none; overflow: hidden;
  white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
  color: var(--text); border: none;
  box-sizing: border-box; max-width: 100%; min-width: 0;
}
.node-box.has-media .node-box-prompt-tagwrap-highlight {
  min-height: 22%; font-size: 0.65625rem;
  background: linear-gradient(to bottom, rgba(10, 10, 12, 0.72) 0%, transparent 100%);
}


/* ---------- Hosted mode: auth overlay + account/billing panel ---------- */

.auth-overlay {
  position: fixed; inset: 0; z-index: 500;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--bg, #0a0a0c) 88%, transparent);
  backdrop-filter: blur(6px);
}
.auth-overlay.hidden { display: none; }
.auth-card {
  width: min(360px, 90vw); padding: 28px; border-radius: var(--radius);
  background: var(--surface-2); border: 1px solid var(--border);
  position: relative;
}
.auth-card-close {
  position: absolute; top: 10px; right: 10px; width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: 50%; cursor: pointer;
  font-size: 0.9375rem; line-height: 1; color: var(--text-faint);
}
.auth-card-close:hover { color: var(--text); background: var(--surface-3, rgba(0, 0, 0, 0.06)); }
.auth-card-close.hidden { display: none; }
.auth-title { font-size: 1.25rem; font-weight: 800; margin: 0 0 4px; color: var(--text); }
.auth-sub { margin: 0 0 18px; font-size: 0.8125rem; color: var(--text-muted); }
.auth-card .field { margin-bottom: 12px; }
.auth-card label { display: block; font-size: 0.71875rem; font-weight: 700; color: var(--text-muted); margin-bottom: 4px; }
.auth-card input {
  width: 100%; padding: 9px 11px; border-radius: 8px; border: 1px solid var(--border);
  background: var(--surface-3); color: var(--text); font-size: 0.84375rem;
}
.auth-toggle { margin: 14px 0 0; text-align: center; font-size: 0.78125rem; color: var(--text-muted); }
.auth-toggle button {
  background: none; border: none; color: var(--accent); font-weight: 700; cursor: pointer; margin-left: 4px;
  font-size: 0.78125rem; padding: 0;
}
#auth-status:empty { display: none; }
.auth-legal-footer {
  margin: 16px 0 0; padding-top: 14px; border-top: 1px solid var(--border);
  text-align: center; font-size: 0.6875rem; line-height: 1.5; color: var(--text-faint);
}
.auth-legal-footer a { color: var(--text-muted); text-decoration: underline; }
.auth-legal-footer a:hover { color: var(--accent); }

/* Only rendered when the auth overlay is being used as a gate on top of
   guest browsing (see openAuthGate()/closeAuthGate() in app.js) — lets a
   guest back out of "sign in to continue" without abandoning the app. */
.auth-gate-dismiss {
  display: block; width: 100%; background: none; border: none; cursor: pointer;
  margin: 10px 0 0; padding: 0; text-align: center; font-size: 0.75rem;
  color: var(--text-faint); text-decoration: underline;
}
.auth-gate-dismiss:hover { color: var(--text-muted); }
.auth-gate-dismiss.hidden { display: none; }

.account-balance-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px; border-radius: var(--radius); background: var(--surface-3); margin-bottom: 18px;
}
.account-balance-number { font-size: 1.75rem; font-weight: 800; color: var(--accent); font-family: var(--font-mono); }
.account-plan-badge {
  font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.3px;
  padding: 5px 12px; border-radius: 999px; border: 1px solid var(--border); color: var(--text-muted);
}
.account-plan-badge.active { color: var(--success); border-color: var(--success); }

.account-plan-grid, .account-topup-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-top: 8px;
}
.account-plan-card, .account-topup-card {
  padding: 14px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--surface-2);
  cursor: pointer; text-align: left; transition: border-color 0.12s ease;
}
.account-plan-card:hover, .account-topup-card:hover { border-color: var(--accent); }
.account-plan-card-name, .account-topup-card-name { font-weight: 700; font-size: 0.84375rem; color: var(--text); }
.account-plan-card-credits, .account-topup-card-credits {
  font-family: var(--font-mono); font-size: 0.71875rem; color: var(--text-muted); margin-top: 4px;
}
/* Recurring-plan-only rollover reminder (see renderBillingGrids in app.js —
   plan cards only, never rendered for one-time top-up cards). Visually
   secondary: smaller and more muted than the credits line right above it. */
.account-plan-card-rollover { font-size: 0.65625rem; color: var(--text-faint); margin-top: 3px; }
.style-card[data-style^="clean-base"] .style-card-name { color: var(--accent); }
.account-plan-card-price, .account-topup-card-price {
  font-weight: 700; font-size: 1rem; color: var(--accent); margin-top: 6px;
}
.account-plan-card-price-period {
  font-weight: 500; font-size: 0.6875rem; color: var(--text-muted); margin-left: 2px;
}

/* ---------- settings page: wide account & billing workspace ---------- */

main:has(> #view-settings.active), main:has(> #view-pricing.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  /* Dots folded directly into main's own background (not a ::before
     pseudo) so the pattern tiles across the full scrollable height
     instead of only the first viewport — see #view-explore's fix above. */
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-settings .settings-canvas::before, #view-pricing .settings-canvas::before { display: none; }
/* Settings is a flex column at least one viewport tall so its info-strip
   footer (see below) sticks to the bottom of the workspace on short pages
   and simply follows the content on long/scrolled ones — same sticky-footer
   technique already used for Projects/Elements, not a viewport-fixed
   overlay. Pricing has no info-strip and stays a plain block. */
#view-settings.active {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  min-height: 100vh;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}
#view-settings .info-strip { margin-top: auto; padding-top: 64px; }
#view-pricing.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}

.settings-security-note {
  display: flex; align-items: center; gap: 8px;
}
.settings-security-icon {
  flex: 0 0 auto;
  width: 22px; height: 22px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 0.6875rem;
}

.settings-billing { margin-top: 22px; }

.settings-top-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
  gap: 20px;
  margin-bottom: 26px;
}
@media (max-width: 900px) { .settings-top-row { grid-template-columns: 1fr; } }

.settings-balance-card, .settings-status-card {
  position: relative;
  padding: 24px 26px;
}
.settings-balance-card {
  border-color: rgba(0, 0, 0, 0.35);
}
.settings-balance-icon {
  position: absolute;
  top: 22px; right: 24px;
  width: 42px; height: 42px;
  border-radius: var(--radius);
  background: var(--accent-soft);
  color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.25rem;
}
.settings-balance-card .section-label { letter-spacing: 0.6px; }
.settings-balance-card .account-balance-number { font-size: 2.125rem; margin-top: 6px; }
.settings-balance-sub { font-size: 0.75rem; color: var(--text-faint); margin-top: 6px; }

.settings-status-rows { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
.settings-status-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding-bottom: 10px; border-bottom: 1px solid var(--border-soft);
}
.settings-status-row:last-child { border-bottom: none; padding-bottom: 0; }
.settings-status-label { font-size: 0.8125rem; color: var(--text-muted); }

.settings-status-card .verify-banner { margin-top: 14px; }
.verify-banner { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.verify-banner-icon {
  flex: 0 0 auto;
  width: 20px; height: 20px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent);
  display: flex; align-items: center; justify-content: center; font-size: 0.65625rem;
}
.verify-banner button { margin-left: auto; }

.settings-section { margin-bottom: 22px; }
.settings-section .section-label-row { margin-bottom: 10px; }
/* Account section: compact horizontal-row card, not a loose stack — same
   .panel surface used elsewhere in the app (the OpenRouter key panel right
   above it, Pricing's balance/status cards), so it reads as one consistent
   card language rather than a bespoke treatment. */
.settings-account-card { padding: 18px 22px; }
.settings-account-card .settings-status-rows { margin-top: 0; }
.settings-account-card .verify-banner { margin-top: 14px; }
.section-label-row h3 {
  font-family: var(--font-display); font-size: 0.9375rem; font-weight: 800; margin: 0;
  text-transform: uppercase; letter-spacing: 0.4px; color: var(--text);
}

.account-plan-grid, .account-topup-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 800px) { .account-plan-grid, .account-topup-grid { grid-template-columns: 1fr; } }

.ai-usage-note {
  font-size: 0.75rem; color: var(--text-faint); margin: 0 0 12px;
}
.ai-usage-summary-row {
  display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 12px;
}
.ai-usage-summary-total, .ai-usage-summary-chip {
  font-size: 0.75rem; padding: 6px 12px; border-radius: 999px;
  border: 1px solid var(--border-soft); color: var(--text-faint);
}
.ai-usage-summary-total { color: var(--text); font-weight: 700; }

/* max-height + overflow-y so a long usage history scrolls inside its own
   box instead of stretching the Settings page taller with every generation
   — keeps the page's overall length (and anything below it) predictable. */
.credit-history-table-wrap {
  overflow-x: auto; overflow-y: auto; max-height: 420px;
  border: 1px solid var(--border-soft); border-radius: var(--radius-lg, 14px);
  /* The rows use the pricing view's dark-on-light text, but this wrap used to
     be transparent — so on the app's dark ground the rows were invisible until
     one was hovered (which painted --surface-2 under it). With no hover on
     touch, rows only lit up when tapped. A solid light card background keeps
     every row readable at all times, on every width. */
  background: var(--surface, #D8DDE1);
}
.credit-history-table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
.credit-history-table th {
  position: sticky; top: 0; background: var(--surface, #141417); z-index: 1;
  text-align: left; padding: 10px 16px; color: var(--text-faint); font-weight: 700;
  font-family: var(--font-mono); font-size: 0.6875rem; letter-spacing: 0.5px; text-transform: uppercase;
  border-bottom: 1px solid var(--border-soft);
}
.credit-history-table td { padding: 12px 16px; border-bottom: 1px solid var(--border-soft); color: var(--text); }
.credit-history-table tbody tr:hover { background: var(--surface-2); }
.credit-history-table tbody tr:last-child td { border-bottom: none; }
.credit-history-delta { font-family: var(--font-mono); font-weight: 700; }
.credit-history-delta.is-positive { color: var(--success, #3cc878); }
.credit-history-delta.is-negative { color: var(--text); }
.credit-history-empty { text-align: center; color: var(--text-faint); padding: 20px 16px; }

.account-plan-card, .account-topup-card {
  position: relative;
  padding: 20px;
}
.account-plan-card.is-current {
  border-color: var(--accent);
  box-shadow: 0 0 18px -6px rgba(0, 0, 0, 0.4);
}
.account-plan-card-current {
  position: absolute; top: 14px; right: 14px;
}
.account-plan-card-highlight {
  position: absolute; top: 14px; right: 14px;
  font-family: var(--font-mono); font-size: 0.59375rem; font-weight: 700; letter-spacing: 0.4px;
  padding: 3px 8px; border-radius: 999px;
  background: rgba(0, 0, 0, 0.14); border: 1px solid var(--accent); color: var(--accent);
}
.account-plan-card:has(.account-plan-card-highlight) .account-plan-card-name {
  padding-right: 78px;
}

.settings-actions-row { display: flex; align-items: center; gap: 14px; }
.settings-manage-btn { width: auto; }
/* Session's Log Out — a real .primary-btn (same black/graphite, white-text,
   rounded button language as every other primary action in the app), just
   constrained to a medium width on desktop instead of the default 100% so
   it reads as a standalone action, not a section-filling bar. Restrained,
   not a destructive/danger-red treatment. */
.settings-logout-btn.primary-btn {
  width: auto; min-width: 180px; margin-top: 0;
}
@media (max-width: 640px) { .settings-logout-btn.primary-btn { width: 100%; } }

/* ---------- pricing page ---------- */
/* Compact top status strip: current balance / current plan / Manage
   Billing, deliberately lighter than the plan cards below it so the
   subscription section stays the visually strongest thing on the page. */
.pricing-status-strip {
  display: flex; align-items: center; gap: 28px;
  padding: 20px 26px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-2);
  margin-bottom: 30px;
}
.pricing-status-block { flex: 0 0 auto; }
.pricing-status-block .account-balance-number { font-size: 1.625rem; margin-top: 4px; }
.pricing-status-plan-name { font-weight: 800; font-size: 1.0625rem; color: var(--text); margin-top: 4px; }
.pricing-status-plan-price { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted); margin-top: 2px; }
.pricing-status-divider { width: 1px; align-self: stretch; background: var(--border-soft); }
.pricing-status-action { margin-left: auto; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.pricing-status-action .status-line { margin: 0; text-align: right; }
@media (max-width: 800px) {
  .pricing-status-strip { flex-wrap: wrap; gap: 16px; padding: 18px 20px; }
  .pricing-status-divider { display: none; }
  .pricing-status-action { margin-left: 0; align-items: flex-start; width: 100%; }
}

/* Subscription plans — the strongest section on the page: bigger cards,
   more breathing room than the demoted top-up packs below. */
.pricing-plans-section .account-plan-grid { gap: 18px; margin-top: 16px; }
/* Five monthly plans (Starter/Creator/Studio, then Ultra/Ultra Plus) laid
   out as 3-then-2 rather than a single cramped 5-up row or a left-aligned
   leftover row. A 6-column grid with every card spanning 2 columns puts
   three full-width cards on row one (3 x 2 = 6 columns, no gaps); row two's
   two cards are pinned to the middle four columns (2/span2 and 4/span2),
   leaving column 1 and column 6 empty as matching side margins — the same
   effect as centering the row, using only the grid this container already
   has rather than a second nested layout. Cards keep the exact same
   per-card width as row one (2 of 6 columns each), so nothing shrinks or
   grows to "fit" five — this scales to a 6th/7th plan later the same way
   (three-per-row, remainder centered) without further changes here. Uses
   nth-child position, not plan identity, so it stays correct regardless of
   which plans exist — see server/billing.js's PLANS object for the actual
   Starter/Creator/Studio/Ultra/Ultra Plus order this assumes.
   Below the 800px breakpoint, the existing `.account-plan-grid,
   .account-topup-grid { grid-template-columns: 1fr }` rule already forces a
   single stacked column — the span/position overrides are reset there too
   so a 2-column grid-column reference never applies against a 1-column
   template. */
.pricing-plans-section .account-plan-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.pricing-plans-section .account-plan-card { grid-column: span 2; }
.pricing-plans-section .account-plan-card:nth-child(4) { grid-column: 2 / span 2; }
.pricing-plans-section .account-plan-card:nth-child(5) { grid-column: 4 / span 2; }
@media (max-width: 800px) {
  .pricing-plans-section .account-plan-grid { grid-template-columns: 1fr; }
  .pricing-plans-section .account-plan-card,
  .pricing-plans-section .account-plan-card:nth-child(4),
  .pricing-plans-section .account-plan-card:nth-child(5) { grid-column: auto; }
}
.pricing-plans-section .account-plan-card { padding: 26px 22px 22px; }
.pricing-plans-section .account-plan-card-name {
  font-family: var(--font-display); font-size: 1rem; letter-spacing: 0.3px; text-transform: uppercase;
}
.pricing-plans-section .account-plan-card-price { font-size: 1.375rem; margin-top: 10px; }
/* "/ month" as a clearly-readable line of its own under the dollar amount,
   not a tiny trailing abbreviation — the price stays the dominant text. */
.pricing-plans-section .account-plan-card-price-period {
  display: block; font-size: 0.78125rem; margin-left: 0; margin-top: 2px;
}
.pricing-plans-section .account-plan-card-credits { font-size: 0.78125rem; margin-top: 8px; }
.account-plan-card-action {
  display: block; margin-top: 18px; text-align: center;
  padding: 10px 14px; border-radius: var(--radius); font-weight: 700; font-size: 0.78125rem;
  letter-spacing: 0.3px; text-transform: uppercase;
  background: #111; color: #fff; border: 1px solid #111;
}
.account-plan-card-action.is-current { background: transparent; color: var(--text-muted); border-color: var(--border); }
.account-plan-card[disabled] { cursor: default; }
.account-plan-card[disabled]:hover { border-color: var(--border); }

/* One-time credit packs — visually secondary: smaller cards, tighter type,
   capped width so they never compete with the plan cards above. */
.pricing-section-sub { margin: -4px 0 14px; font-size: 0.78125rem; color: var(--text-faint); }
/* Rollover disclosure — visually secondary (smaller, muted) so it reads as
   a clarifying footnote under the Monthly Plans helper text above, not a
   second headline competing with it. */
.pricing-rollover-disclosure { margin: -10px 0 14px; font-size: 0.71875rem; color: var(--text-faint); }
/* "Credits refresh <date>"/"on your billing date" note under the current
   plan's price in the status strip — same muted treatment. */
.pricing-status-refresh-note { font-size: 0.6875rem; color: var(--text-faint); margin-top: 4px; }
/* Same 3-column grid/width as the plan cards above (no max-width cap) so
   the three packs spread evenly across the full content row instead of
   clustering on the left with empty space on the right — still visually
   secondary via smaller padding/type, not a narrower container. */
.pricing-topup-section .account-topup-grid { gap: 14px; }
.pricing-topup-section .account-topup-card { padding: 14px 16px; }
.pricing-topup-section .account-topup-card-name { font-size: 0.78125rem; }
.pricing-topup-section .account-topup-card-price { font-size: 0.875rem; margin-top: 4px; }
/* Tablet: let the 3 packs fall back to a natural 2+1 wrap before the
   shared 800px breakpoint (below) forces a single column for mobile. */
@media (min-width: 801px) and (max-width: 1100px) {
  .pricing-topup-section .account-topup-grid { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
}
.account-topup-card-action {
  display: block; margin-top: 10px; text-align: center;
  padding: 7px 10px; border-radius: var(--radius); font-weight: 700; font-size: 0.6875rem;
  letter-spacing: 0.2px; text-transform: uppercase;
  background: #111; color: #fff; border: 1px solid #111;
}

/* Recent activity — demoted (short, fixed height) with a link out to the
   same complete table Settings used to show unconditionally. */
.credit-history-table-wrap-compact { max-height: none; }
.pricing-view-full-history-btn {
  background: none; border: none; color: var(--text-muted);
  font-family: var(--font-body); font-size: 0.78125rem; font-weight: 700; cursor: pointer;
  padding: 12px 2px 0; display: block;
}
.pricing-view-full-history-btn:hover { color: var(--text); }
#credit-history-full-wrap { margin-top: 14px; }

/* ---------- profile page ---------- */

main:has(> #view-profile.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  background:
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
}
main:has(> #view-profile.active)::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: radial-gradient(var(--black-dots) 1px, transparent 1px);
  background-size: 22px 22px;
  pointer-events: none;
  z-index: 0;
}
#view-profile .profile-canvas::before { display: none; }
#view-profile.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1440px;
  margin-left: auto;
  margin-right: auto;
}

.profile-identity-card {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 26px 28px;
  margin-top: 6px;
  border-color: rgba(0, 0, 0, 0.3);
}
@media (max-width: 720px) { .profile-identity-card { flex-wrap: wrap; } }

.profile-avatar-wrap { position: relative; flex-shrink: 0; }
.profile-avatar {
  width: 84px; height: 84px; border-radius: 50%;
  background: linear-gradient(145deg, var(--surface-3), var(--surface-2));
  border: 1px solid var(--border);
  box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.2), var(--inset-hairline);
  display: flex; align-items: center; justify-content: center;
  color: var(--accent); font-size: 1.875rem; font-weight: 800; font-family: var(--font-display);
  overflow: hidden;
}
.profile-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.profile-avatar-edit {
  position: absolute; bottom: -2px; right: -2px;
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--accent); color: #ffffff;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.75rem; cursor: pointer; border: 2px solid var(--surface);
}
.profile-avatar-edit:hover { filter: brightness(1.08); }

.profile-identity-body { flex: 1; min-width: 0; }
.profile-identity-name { font-family: var(--font-display); font-size: 1.5rem; font-weight: 800; }
.profile-identity-role { font-size: 0.875rem; color: var(--text-muted); margin-top: 2px; }
.profile-identity-body .section-hint { margin-top: 8px; }
.profile-identity-meta {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin-top: 12px; font-size: 0.78125rem; color: var(--text-faint);
}

.profile-identity-actions { display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; }
.profile-identity-actions .secondary-btn { white-space: nowrap; }

.profile-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; }
@media (max-width: 900px) { .profile-grid { grid-template-columns: 1fr; } }

.profile-info-rows { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }

.profile-details-card .field { margin-bottom: 14px; }
.profile-details-card .primary-btn { width: auto; padding: 11px 24px; margin-top: 4px; }

/* ---------- Admin dashboard ---------- */

main:has(> #view-admin.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  /* Dots folded directly into main's own background (not a ::before
     pseudo) so the pattern tiles across the full scrollable height
     instead of only the first viewport — see #view-explore's fix above. */
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-admin .admin-canvas::before { display: none; }
#view-admin.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1600px;
  margin-left: auto;
  margin-right: auto;
}

.admin-stats-grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}
@media (max-width: 1300px) { .admin-stats-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 700px) { .admin-stats-grid { grid-template-columns: repeat(2, 1fr); } }

.admin-stat-card {
  padding: 20px 18px;
  border-radius: var(--radius-lg);
  background: var(--surface);
  border: 1px solid var(--border);
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
.admin-stat-card:hover {
  border-color: rgba(0, 0, 0, 0.4);
  box-shadow: var(--shadow-glow);
  transform: translateY(-2px);
}
.admin-stat-icon {
  width: 38px; height: 38px;
  border-radius: var(--radius);
  background: var(--accent-soft);
  color: var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-size: 1rem;
  margin-bottom: 14px;
}
.admin-stat-number { font-size: 1.625rem; font-weight: 800; font-family: var(--font-mono); color: var(--accent); }
.admin-stat-label { font-size: 0.71875rem; color: var(--text-faint); margin-top: 4px; }

.admin-accounts-panel .section-label { margin-bottom: 4px; }

.admin-users-table-wrap { overflow-x: auto; margin-top: 12px; }
.admin-users-table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
.admin-users-table th {
  text-align: left; padding: 10px 16px; color: var(--text-faint); font-weight: 700;
  font-family: var(--font-mono); font-size: 0.6875rem; letter-spacing: 0.5px; text-transform: uppercase;
  border-bottom: 1px solid var(--border-soft);
}
.admin-users-table td { padding: 14px 16px; border-bottom: 1px solid var(--border-soft); color: var(--text); }
.admin-users-table tbody tr:hover { background: var(--surface-2); }
.admin-users-table tbody tr:last-child td { border-bottom: none; }

.admin-status-dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 999px; margin-right: 7px;
  vertical-align: middle;
}
.admin-status-dot.is-yes { background: var(--success); box-shadow: 0 0 6px rgba(60, 200, 120, 0.6); }
.admin-status-dot.is-no { background: var(--text-faint); }

.admin-credit-action { display: inline-flex; align-items: center; gap: 8px; }
.admin-verify-link-out { margin-top: 6px; font-size: 0.75rem; }
.admin-verify-link-out a { text-decoration: none; }
.admin-users-table input[type="number"] {
  width: 84px; padding: 7px 9px; border-radius: var(--radius); border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text); font-size: 0.78125rem;
}
.admin-apply-btn {
  padding: 7px 14px; border-radius: var(--radius); border: 1px solid var(--accent); background: transparent;
  color: var(--accent); font-size: 0.75rem; font-weight: 700; cursor: pointer; white-space: nowrap;
}
.admin-apply-btn:hover { background: var(--accent-soft); }
/* Promo column: a compact status line above the grant form, and a narrower
   days input so "credits + days + Grant" fits one row. */
.admin-promo-days { width: 56px !important; }
.admin-promo-status { font-size: 0.71875rem; color: var(--text-faint); margin-bottom: 6px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.admin-promo-status.is-expired { color: var(--danger, #d9534f); }
.admin-promo-reclaim { border-color: var(--danger, #d9534f) !important; color: var(--danger, #d9534f) !important; padding: 4px 10px !important; }
.admin-promo-reclaim:hover { background: rgba(217, 83, 79, 0.12) !important; }

/* ---------- Admin: Contact messages inbox ---------- */

.admin-contact-panel { margin-top: 20px; }
.admin-contact-unread-badge {
  display: inline-block; margin-left: 8px; padding: 2px 8px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent); font-size: 0.65625rem; font-weight: 700;
  letter-spacing: 0.4px; text-transform: none; vertical-align: middle;
}
.admin-contact-layout {
  display: grid; grid-template-columns: minmax(220px, 320px) minmax(0, 1fr); gap: 16px; margin-top: 12px;
  min-height: 320px;
}
@media (max-width: 1100px) { .admin-contact-layout { grid-template-columns: minmax(180px, 260px) minmax(0, 1fr); } }
@media (max-width: 860px) { .admin-contact-layout { grid-template-columns: 1fr; } }

.admin-contact-list {
  display: flex; flex-direction: column; gap: 6px; max-height: 520px; overflow-y: auto;
  border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 8px;
}
.admin-contact-row {
  display: flex; flex-direction: column; gap: 3px; text-align: left; width: 100%;
  padding: 10px 12px; border-radius: var(--radius); border: 1px solid transparent;
  background: none; cursor: pointer; color: var(--text);
}
.admin-contact-row:hover { background: var(--surface-2); }
.admin-contact-row.active { background: var(--surface-2); border-color: var(--border); }
.admin-contact-row.unread .admin-contact-row-name::before {
  content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 999px;
  background: var(--accent); margin-right: 6px; vertical-align: middle;
}
.admin-contact-row-top { display: flex; justify-content: space-between; gap: 8px; font-size: 0.78125rem; }
.admin-contact-row-name { font-weight: 700; }
.admin-contact-row-date { color: var(--text-faint); font-size: 0.6875rem; white-space: nowrap; }
.admin-contact-row-email { color: var(--text-muted); font-size: 0.71875rem; }
.admin-contact-row-preview { color: var(--text-faint); font-size: 0.71875rem; }

.admin-contact-detail {
  border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 18px;
}
.admin-contact-detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
.admin-contact-detail-name { font-weight: 700; font-size: 0.9375rem; color: var(--text); }
.admin-contact-detail-email { color: var(--text-muted); font-size: 0.78125rem; margin-top: 2px; }
.admin-contact-detail-actions { display: flex; gap: 8px; }
.admin-contact-detail-actions .secondary-btn { text-decoration: none; }
.admin-contact-detail-date { color: var(--text-faint); font-size: 0.71875rem; margin-top: 12px; }
.admin-contact-detail-message {
  margin-top: 10px; color: var(--text); font-size: 0.84375rem; line-height: 1.6; white-space: pre-wrap;
}

/* ---------- Email verification banner ---------- */

.verify-banner {
  margin-top: 14px; padding: 10px 14px; border-radius: var(--radius);
  background: color-mix(in srgb, var(--warning) 14%, var(--surface-3)); border: 1px solid color-mix(in srgb, var(--warning) 45%, var(--border));
  font-size: 0.78125rem; color: var(--text);
}
.verify-banner button {
  margin-left: 8px; background: none; border: none; color: var(--accent); font-weight: 700; cursor: pointer; font-size: 0.78125rem;
}

/* ---------- Scene workspace: subtle Ember dots across the whole page ----------
   Same low-contrast dotted texture used elsewhere in the app, applied to the
   entire "Direct the scene" workspace rather than just the output preview.
   Sits behind everything (z-index 0); every existing control/panel keeps its
   own solid background, so the dots only ever show in the open canvas space
   around them — nothing about the controls themselves changes. */
/* Superseded by the full-bleed dot layer on `main` (see
   main:has(> #view-scene.active) above), which spans the true
   edge-to-edge workspace instead of just this content column. */
#view-scene { position: relative; }
#view-scene::before { display: none; }
#view-scene > * { position: relative; z-index: 1; }

/* ---------- Global Ember background system, part 2 ----------
   Image / Video / Characters / Character Sheet get the same page-wide dots
   + restrained amber atmosphere as Scene/Studio/Explore/Projects/Elements
   (the "strong" tier — creation/discovery pages). Workflow keeps its own
   functional node-editor grid (a working dot canvas for aligning nodes,
   not decorative) untouched and just gets the amber atmosphere layered
   behind it. Home is a full-bleed looping video with no open canvas space
   for a texture to sit in, so it's intentionally left alone. Sidebar
   (.rail) is never touched by any of this. */
#view-image, #view-video, #view-characters, #view-charsheet { position: relative; }
#view-image::before, #view-video::before, #view-characters::before, #view-charsheet::before {
  content: '';
  position: absolute; inset: 0;
  background-image: radial-gradient(var(--black-dots) 1px, transparent 1px);
  background-size: 22px 22px;
  pointer-events: none;
  z-index: 0;
}
#view-image > *, #view-video > *, #view-characters > *, #view-charsheet > * { position: relative; z-index: 1; }
/* Image page only: the shared dot layer above is superseded by the
   full-bleed one painted on `main` (see main:has(> #view-image.active)
   above), which spans the true edge-to-edge workspace instead of just
   this content column. */
#view-image::before { display: none; }
/* Video page only: same reasoning — superseded by the full-bleed layer on
   `main` (see main:has(> #view-video.active) above). */
#view-video::before { display: none; }
/* Characters page only: same reasoning — superseded by the full-bleed
   layer on `main` (see main:has(> #view-characters.active) above). */
#view-characters::before { display: none; }
/* Character Sheet page only: same reasoning — superseded by the
   full-bleed layer on `main` (see main:has(> #view-charsheet.active)
   above). */
#view-charsheet::before { display: none; }

#view-workflow { position: relative; }
#view-workflow > * { position: relative; z-index: 1; }


/* ---------- Prompt Vault: Back button row ----------
   Reuses the exact same .mobile-back-btn component every other creative
   page already shows at the top of its .panel-head — Prompt Vault has no
   .panel-head (its own dark topbar/catbar header instead), so this row is
   the equivalent top-of-page slot for that identical button. Previously
   hidden above 860px, which is what left Prompt Vault without a Back
   button on desktop while every other page had one. */
.vault-mobile-back-row { display: block; padding: 14px 36px 0; }
@media (max-width: 860px) {
  .vault-mobile-back-row { padding: 14px 16px 0; }
}
/* #view-vault * below resets margin/padding on every descendant (it needs
   that for its own dark-theme components) with higher specificity than the
   shared .mobile-back-btn rule, which would otherwise flatten this button's
   pill padding — restore the exact same padding value .mobile-back-btn
   already uses everywhere else, so it renders identically here. */
/* margin-bottom gives a clear gap before the sticky category bar that follows
   the Back button (they were flush at 0px on mobile, reading as an overlap).
   This selector's specificity beats the #view-vault * padding/margin reset. */
#view-vault .mobile-back-btn { padding: 7px 12px; margin-bottom: 16px; }

/* ---------- Prompt Vault: page styles (scoped to #view-vault, adapted to Ember tokens) ---------- */

  #view-vault {
    /* The one intentional dark workspace in the app — a black archive
       cabinet inside the brushed-silver console. Neutral black/graphite/
       silver/white only, no warm tones. --accent here is a light "polished
       silver" highlight (used for selected tabs, hover accents, medium-
       silver labels) rather than the app-wide black --accent token. */
    --bg: #0a0a0c; --sidebar: #141417; --card: #1a1a1f; --card-hover: #212127;
    --border: #3a3b40; --text: #f5f5f4; --muted: #9aa0a6; --accent: #e7eaec;
    --accent-soft: rgba(255, 255, 255, 0.06); --pill-bg: #1a1a1f;
    --text-faint: #9aa0a6;
    --text-muted: #c7cbd1;
  }
  #view-vault * { margin:0; padding:0; box-sizing:border-box; }
  #view-vault { background:var(--bg); color:var(--text); font-family: var(--font-body); }
  /* Category menu: horizontal bar across the top of the content area
     (not a second vertical sidebar) — scrollable, no visible scrollbar. */
  #view-vault .vault-catbar { display:flex; flex-direction:row; align-items:center; border-bottom:1px solid var(--border); background:rgba(8,8,9,0.85); backdrop-filter:blur(12px); position:sticky; top:0; z-index:49; }
  #view-vault .nav-scroll { flex:1 1 auto; min-width:0; display:flex; flex-direction:row; flex-wrap:nowrap; align-items:center; overflow-x:auto; overflow-y:hidden; gap:6px; padding:12px 14px; scrollbar-width:none; -ms-overflow-style:none; scroll-behavior:smooth; }
  #view-vault .nav-scroll::-webkit-scrollbar { display:none; width:0; height:0; }
  /* Fixed left/right scroll controls flanking the category strip — they
     live outside the scrolling #nav element so they never move with the
     pills underneath them and never disappear because content scrolled.
     Disabled state (real scroll boundary reached) is computed in app.js
     from the strip's own scrollLeft/scrollWidth — this is a finite
     scroller, never a wrapping/looping carousel. */
  #view-vault .vault-catnav {
    flex:0 0 auto; width:32px; height:32px; margin:0 10px;
    display:flex; align-items:center; justify-content:center;
    border-radius:50%; border:1px solid var(--border); background:var(--pill-bg);
    color:var(--text); font-size: 1rem; line-height:1; cursor:pointer;
    transition:background 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
  }
  #view-vault .vault-catnav:hover:not(:disabled) { background:var(--card-hover); border-color:var(--accent); }
  #view-vault .vault-catnav:disabled { opacity:0.35; cursor:default; }
  #view-vault .nav-pill { display:inline-flex; align-items:center; justify-content:center; width:auto; flex:0 0 auto; text-align:center; white-space:nowrap; background:var(--pill-bg); border:1px solid var(--border); color:var(--muted); font-family:var(--font-body); font-size: 0.8125rem; font-weight:500; padding:9px 16px; border-radius:100px; cursor:pointer; transition:all 0.18s ease; }
  #view-vault .nav-pill:hover { background:var(--card-hover); color:var(--text); }
  #view-vault .nav-pill.active { background:var(--accent); color:#0a0a0b; font-weight:600; border-color:var(--accent); }
  #view-vault .topbar-ig { display:inline-flex; align-items:center; gap:7px; font-size: 0.75rem; color:var(--muted); text-decoration:none; margin-right:18px; }
  #view-vault .topbar-ig:hover { color:var(--accent); }
  #view-vault .main { flex:1; min-width:0; display:flex; flex-direction:column; }
  #view-vault .topbar { height:72px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; padding:0 36px; background:rgba(8,8,9,0.85); backdrop-filter:blur(12px); position:sticky; top:0; z-index:50; }
  #view-vault .page-title { font-family:var(--font-display); font-size: 1.375rem; font-weight:400; color:var(--text); }
  #view-vault .page-count { font-size: 0.75rem; color:var(--muted); font-weight:500; margin-left:12px; }
  #view-vault .search-wrap { position:relative; }
  #view-vault .search-input { background:var(--silver-polished); border:1px solid var(--pure-black); border-radius:100px; padding:10px 18px 10px 40px; width:260px; color:var(--text-primary-black); font-size: 0.8125rem; font-family:var(--font-body); outline:none; }
  #view-vault .search-input:focus { border-color:var(--pure-black); box-shadow: 0 0 0 3px rgba(10, 10, 10, 0.12); }
  #view-vault .search-input::placeholder { color:var(--text-secondary-dark); }
  #view-vault .search-icon { position:absolute; left:14px; top:50%; transform:translateY(-50%); color:var(--text-secondary-dark); pointer-events:none; }
  #view-vault .content { padding:32px 36px 80px; }
  #view-vault .cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(340px,1fr)); gap:20px; }
  #view-vault .placard { background:var(--card); border-radius:16px; overflow:hidden; display:flex; flex-direction:column; border:1px solid transparent; transition:background 0.2s,border-color 0.2s,transform 0.2s; }
  #view-vault .placard:hover { background:var(--card-hover); border-color:var(--border); transform:translateY(-2px); }
  #view-vault .placard-top { padding:22px 22px 0; }
  /* Permanent Prompt Vault ID — medium silver, distinct from the
     category/accent info (.meta-left above). Compact, sits just above
     the existing meta row; never re-numbered by category or search. */
  #view-vault .placard-id { display:block; font-size: 0.65625rem; font-weight:600; letter-spacing:0.08em; color:var(--text-faint); margin-bottom:6px; }
  #view-vault .placard-meta { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
  #view-vault .meta-left { font-size: 0.625rem; font-weight:600; letter-spacing:0.12em; text-transform:uppercase; color:var(--accent); }
  #view-vault .stars { font-size: 0.6875rem; color:var(--accent); letter-spacing:1px; }
  #view-vault .placard-title { font-family:var(--font-display); font-size: 1.25rem; font-weight:400; line-height:1.25; color:var(--text); margin-bottom:16px; }
  #view-vault .placard-body { padding:0 22px 18px; flex:1; display:flex; flex-direction:column; gap:14px; }
  /* Optional video example — only present when a card actually has one
     (see app.js); text-only cards never get this markup at all, so they
     take up no extra space and need no CSS override here. */
  #view-vault .placard-video-wrap {
    position:relative; margin:0 22px 14px; aspect-ratio:16/9; border-radius:10px;
    overflow:hidden; background:#000; cursor:pointer;
  }
  #view-vault .placard-video-wrap video { width:100%; height:100%; object-fit:cover; display:block; }
  #view-vault .placard-video-play {
    position:absolute; inset:0; margin:auto; width:44px; height:44px; border-radius:50%;
    background:rgba(10,10,12,0.72); border:1px solid var(--border); color:#fff;
    display:flex; align-items:center; justify-content:center; font-size: 0.875rem; padding-left:2px;
    cursor:pointer; transition:background 0.15s ease, opacity 0.15s ease;
  }
  #view-vault .placard-video-play:hover { background:rgba(0,0,0,0.85); }
  #view-vault .placard-video-wrap.is-playing .placard-video-play { opacity:0; pointer-events:none; }
  #view-vault .avoid-box { background:#101013; border:1px solid var(--border); border-radius:10px; padding:12px 14px; font-size: 0.71875rem; line-height:1.55; color:var(--text-muted); }
  #view-vault .avoid-label { font-size: 0.5625rem; font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:var(--accent); margin-bottom:5px; }
  #view-vault .prompt { font-size: 0.84375rem; line-height:1.65; color:var(--text); opacity:0.92; }
  #view-vault .placard-bottom { padding:14px 22px 18px; display:flex; align-items:center; justify-content:space-between; gap:12px; border-top:1px solid var(--border); margin-top:auto; }
  #view-vault .chip-row { display:flex; flex-wrap:wrap; gap:6px; }
  #view-vault .chip { font-size: 0.625rem; font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:var(--muted); background:var(--pill-bg); padding:4px 10px; border-radius:100px; }
  #view-vault .copy-pill { background:#202024; border:1px solid var(--border); color:var(--text); font-family:var(--font-body); font-size: 0.6875rem; font-weight:600; letter-spacing:0.06em; text-transform:uppercase; padding:7px 16px; border-radius:100px; cursor:pointer; transition:all 0.18s; white-space:nowrap; }
  #view-vault .copy-pill:hover { background:#26262b; border-color:var(--accent); color:var(--accent); }
  #view-vault .copy-pill.copied { background:rgba(74,124,89,0.2); border-color:#4a7c59; color:#8fbf9a; }
  #view-vault .hidden { display:none !important; }

  /* How it works */
  #view-vault .how-section {
    margin-top: 64px;
    padding-top: 48px;
    border-top: 1px solid var(--border);
    max-width: 720px;
  }
  #view-vault .how-title {
    font-family: var(--font-display);
    font-size: 1.375rem;
    font-weight: 400;
    color: var(--text);
    margin-bottom: 24px;
  }
  #view-vault .how-block {
    margin-bottom: 28px;
  }
  #view-vault .how-block h3 {
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--accent);
    margin-bottom: 10px;
  }
  #view-vault .how-block p {
    font-size: 0.875rem;
    line-height: 1.7;
    color: var(--muted);
  }
  #view-vault .how-block p strong {
    color: var(--text);
    font-weight: 500;
  }
  #view-vault .how-footer {
    margin-top: 36px;
    padding-top: 24px;
    border-top: 1px solid var(--border);
    font-size: 0.75rem;
    color: var(--muted);
  }
  #view-vault .how-footer a {
    color: var(--accent);
    text-decoration: none;
  }
  #view-vault .how-footer a:hover { text-decoration: underline; }

  @media (max-width:900px) {
    #view-vault { flex-direction:column; }
    #view-vault .topbar { padding:0 20px; }
    #view-vault .content { padding:24px 20px 60px; }
    #view-vault .search-input { width:180px; }
  }


/* ===== MOBILE-SAFE CSS-ONLY CATEGORY NAV ===== */
#view-vault .vault-state { position:fixed; opacity:0; pointer-events:none; width:1px; height:1px; }
#view-vault .app { display:flex; width:100%; min-height:100vh; }
#view-vault .nav-pill { user-select:none; -webkit-user-select:none; }
#view-vault .nav-pill label { all:unset; }
/* selected pill */
#view-vault #state-all:checked ~ .app label[for="state-all"], #view-vault #state-kinetic:checked ~ .app label[for="state-kinetic"], #view-vault #state-stylized:checked ~ .app label[for="state-stylized"], #view-vault #state-lens:checked ~ .app label[for="state-lens"], #view-vault #state-product:checked ~ .app label[for="state-product"], #view-vault #state-dialogue:checked ~ .app label[for="state-dialogue"], #view-vault #state-observational:checked ~ .app label[for="state-observational"], #view-vault #state-entry:checked ~ .app label[for="state-entry"], #view-vault #state-texture:checked ~ .app label[for="state-texture"], #view-vault #state-dread:checked ~ .app label[for="state-dread"], #view-vault #state-light:checked ~ .app label[for="state-light"], #view-vault #state-solo:checked ~ .app label[for="state-solo"], #view-vault #state-track:checked ~ .app label[for="state-track"], #view-vault #state-elemental:checked ~ .app label[for="state-elemental"], #view-vault #state-subjective:checked ~ .app label[for="state-subjective"], #view-vault #state-speculative:checked ~ .app label[for="state-speculative"], #view-vault #state-athletic:checked ~ .app label[for="state-athletic"], #view-vault #state-temporal:checked ~ .app label[for="state-temporal"], #view-vault #state-how:checked ~ .app label[for="state-how"] { background:var(--accent); color:#0a0a0b; font-weight:600; }
/* category filtering: pure CSS, no JS */
#view-vault #state-kinetic:checked ~ .app .placard:not([data-cat="kinetic"]), #view-vault #state-stylized:checked ~ .app .placard:not([data-cat="stylized"]), #view-vault #state-lens:checked ~ .app .placard:not([data-cat="lens"]), #view-vault #state-product:checked ~ .app .placard:not([data-cat="product"]), #view-vault #state-dialogue:checked ~ .app .placard:not([data-cat="dialogue"]), #view-vault #state-observational:checked ~ .app .placard:not([data-cat="observational"]), #view-vault #state-entry:checked ~ .app .placard:not([data-cat="entry"]), #view-vault #state-texture:checked ~ .app .placard:not([data-cat="texture"]), #view-vault #state-dread:checked ~ .app .placard:not([data-cat="dread"]), #view-vault #state-light:checked ~ .app .placard:not([data-cat="light"]), #view-vault #state-solo:checked ~ .app .placard:not([data-cat="solo"]), #view-vault #state-track:checked ~ .app .placard:not([data-cat="track"]), #view-vault #state-elemental:checked ~ .app .placard:not([data-cat="elemental"]), #view-vault #state-subjective:checked ~ .app .placard:not([data-cat="subjective"]), #view-vault #state-speculative:checked ~ .app .placard:not([data-cat="speculative"]), #view-vault #state-athletic:checked ~ .app .placard:not([data-cat="athletic"]), #view-vault #state-temporal:checked ~ .app .placard:not([data-cat="temporal"]), #view-vault #state-how:checked ~ .app .placard { display:none !important; }
#view-vault #state-how:not(:checked) ~ .app .how-section { display:none; }
#view-vault #state-how:checked ~ .app .how-section { display:block; margin-top:0; padding-top:0; border-top:none; }
/* title variants */
#view-vault .state-title { display:none; }
#view-vault #state-all:checked ~ .app .title-all, #view-vault #state-kinetic:checked ~ .app .title-kinetic, #view-vault #state-stylized:checked ~ .app .title-stylized, #view-vault #state-lens:checked ~ .app .title-lens, #view-vault #state-product:checked ~ .app .title-product, #view-vault #state-dialogue:checked ~ .app .title-dialogue, #view-vault #state-observational:checked ~ .app .title-observational, #view-vault #state-entry:checked ~ .app .title-entry, #view-vault #state-texture:checked ~ .app .title-texture, #view-vault #state-dread:checked ~ .app .title-dread, #view-vault #state-light:checked ~ .app .title-light, #view-vault #state-solo:checked ~ .app .title-solo, #view-vault #state-track:checked ~ .app .title-track, #view-vault #state-elemental:checked ~ .app .title-elemental, #view-vault #state-subjective:checked ~ .app .title-subjective, #view-vault #state-speculative:checked ~ .app .title-speculative, #view-vault #state-athletic:checked ~ .app .title-athletic, #view-vault #state-temporal:checked ~ .app .title-temporal, #view-vault #state-how:checked ~ .app .title-how { display:inline; }
@media (max-width:900px) {
  #view-vault .app { flex-direction:column; }

  /* Compact horizontal pill slider — keeps the CSS-only navigation working */
  #view-vault .nav-scroll {
    flex-direction:row;
    flex-wrap:nowrap;
    overflow-x:auto;
    overflow-y:hidden;
    padding:0 16px 6px;
    gap:7px;
    scroll-snap-type:x proximity;
    -webkit-overflow-scrolling:touch;
    scrollbar-width:none;
    overscroll-behavior-x:contain;
  }
  #view-vault .nav-scroll::-webkit-scrollbar { display:none; }
  #view-vault .vault-catnav { width:28px; height:28px; margin:0 6px; font-size: 0.875rem; }
  #view-vault .nav-pill {
    width:auto;
    min-height:38px;
    flex:0 0 auto;
    display:inline-flex;
    align-items:center;
    justify-content:center;
    padding:8px 14px;
    border:1px solid var(--border);
    background:var(--pill-bg);
    white-space:nowrap;
    scroll-snap-align:start;
  }
  #view-vault .nav-pill[for="state-how"] { margin-top:0 !important; }

  #view-vault .topbar { position:sticky; top:0; height:auto; min-height:64px; padding:12px 16px; gap:12px; flex-wrap:wrap; }
  #view-vault .search-input { width:min(48vw,220px); }
  #view-vault .cards { grid-template-columns:1fr; }
}

/* ---------- Prompt Vault (desktop): reclaim full content width, and make
   the category bar + prompt-card area scroll independently of everything
   else — same fixed-sidebar / independently-scrolling pattern main already
   uses elsewhere (see "fixed sidebar / independently-scrolling main" above).
   Main Ember sidebar (.rail) is untouched by this. ---------- */
@media (min-width: 861px) {
  main:has(> #view-vault.active) {
    max-width: none;
    padding: 0;
    height: 100vh;
    overflow: hidden;
  }
  #view-vault { height: 100%; }
  #view-vault .vault-canvas { height: 100%; }
  #view-vault .app { height: 100%; min-height: 0; }
  #view-vault .main { height: 100%; min-height: 0; }
  #view-vault .content {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  #view-vault .content::-webkit-scrollbar { display: none; width: 0; height: 0; }
}

.project-hub-tile-icon-glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2.125rem;
  color: var(--text-muted);
  background: var(--surface-2);
}

/* ---------- Remaster ---------- */
/* The dot grid used to be a separate ::before box (position:absolute;
   inset:0) sized to `main`'s own padding box. That works fine on pages
   whose content fits in one viewport, but `main` is itself the scroll
   container on desktop (height:100vh; overflow-y:auto — see the
   min-width:861px block above), so an absolutely-positioned child scrolls
   away with the rest of the content: the dots only ever covered the
   first screenful, leaving the History section (and anything else below
   the fold) sitting on plain silver with no dots. Folding the dot layer
   into `main`'s own `background` shorthand instead fixes this: with the
   default background-attachment:scroll, a background painted directly on
   a scrolling box stays anchored to the box's own viewport and is
   repainted across the full visible area at any scroll position, exactly
   like the metallic gradient layers below already do. No ::before is
   needed anymore. */
main:has(> #view-remaster.active) {
  margin: 0;
  max-width: none;
  padding: 0;
  position: relative;
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px) 0 0 / 22px 22px,
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
}
#view-remaster.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  padding: 36px 44px 60px;
  max-width: 1560px;
  margin-left: 0;
  margin-right: auto;
}

/* Left control column narrower than the right Before/After viewer.
   align-items: stretch (a CSS grid default-fighting override) makes both
   grid cells match the height of the taller one — the control panel's own
   content height — so the viewer panel's top and bottom edges line up with
   the control panel's exactly, without hardcoding either height. */
.remaster-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  gap: 20px;
  align-items: stretch;
  margin-bottom: 22px;
}
@media (max-width: 900px) {
  .remaster-layout { grid-template-columns: 1fr; align-items: start; }
}

/* Mobile-only: surface the Before/After viewer higher on Enhance, right
   after the Media Type / Upscale-Remaster selectors and above the
   upload/preset controls card. Desktop keeps the side-by-side
   controls-left/viewer-right layout via .remaster-layout's two-column grid
   above — this only reorders the two panels once they've stacked into one
   column, and it's shared by both the image and video Enhance panels (same
   .remaster-layout class on each), so neither panel needs a separate rule
   and nothing is duplicated. */
@media (max-width: 900px) {
  .remaster-controls-panel { order: 2; }
  .remaster-viewer-panel { order: 1; }
}
.remaster-controls-panel,
.remaster-viewer-panel {
  margin-bottom: 0;
}
/* .viewfinder-panel is already display:flex; flex-direction:column — this
   just lets it actually grow to fill the stretched grid-cell height instead
   of stopping at its own min-height. */
.remaster-viewer-panel {
  height: 100%;
}
@media (max-width: 900px) {
  .remaster-viewer-panel { height: auto; }
}

.remaster-detect-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  padding: 10px 12px;
}
.remaster-detect-row #remaster-detect-status {
  font-size: 0.8125rem;
  color: var(--text-muted);
}
.remaster-detect-row select {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  color: var(--text);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 0.8125rem;
}

.remaster-strength-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.remaster-strength-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 10px 6px;
  border-radius: var(--radius);
  border: 1px solid var(--border-soft);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, transform 0.1s ease;
}
.remaster-strength-btn:hover { background: var(--surface-3); }
.remaster-strength-btn.active {
  background: linear-gradient(160deg, #0c0c0c, #232323);
  border-color: #050505;
  color: #fff;
}
.remaster-strength-label { font-size: 0.8125rem; font-weight: 600; }
.remaster-strength-helper { font-size: 0.6875rem; color: var(--text-faint); }
.remaster-strength-btn.active .remaster-strength-helper { color: rgba(255, 255, 255, 0.7); }

.remaster-model-info {
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  padding: 9px 12px;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* Before/After viewer: original and remastered images share the exact
   same box, remastered layer clipped by the divider position rather than
   the two ever being placed side by side. .remaster-viewfinder rides on
   top of the base .viewfinder class (flex: 1), so on desktop it grows to
   fill the full stretched panel height (see .remaster-viewer-panel above)
   instead of stopping at a fixed px/vh cap. */
.remaster-viewfinder {
  position: relative;
  min-height: 420px;
  overflow: hidden;
  background: var(--vf-bg);
}
.remaster-compare {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  user-select: none;
  touch-action: none;
}
/* Plain single-image state (source preview before a result exists) — a
   bare <img> direct child of .remaster-compare, not wrapped in
   before/after layers. */
.remaster-compare > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  pointer-events: none;
}
.remaster-compare-before,
.remaster-compare-after {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
/* Both layers' images are sized identically (100%/100%, object-fit:
   contain, centered) so the Before and After frames land in the exact
   same place regardless of the source aspect ratio — required for the
   divider to line up perfectly as it's dragged. */
.remaster-compare-before img,
.remaster-compare-after img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  pointer-events: none;
}
.remaster-compare-after {
  clip-path: inset(0 50% 0 0);
}
.remaster-compare-label {
  position: absolute;
  top: 10px;
  z-index: 2;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: #fff;
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 999px;
  padding: 3px 10px;
  pointer-events: none;
}
.remaster-compare-label-before { left: 10px; }
.remaster-compare-label-after { right: 10px; }
.remaster-compare-divider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  background: linear-gradient(180deg, #fff, #c7c7c7);
  transform: translateX(-50%);
  z-index: 3;
  cursor: ew-resize;
}
.remaster-compare-handle {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: linear-gradient(160deg, #f2f2f2, #b9b9b9);
  border: 1px solid rgba(0, 0, 0, 0.4);
  box-shadow: 0 3px 10px -3px rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: ew-resize;
  font-size: 0.75rem;
  color: #111;
}
.remaster-compare-handle::before { content: '◂▸'; letter-spacing: 2px; }

@media (max-width: 900px) {
  /* Mobile stacks the panels vertically (see .remaster-layout above) — the
     "match the input card's height" rule is a desktop side-by-side concept
     only, so on mobile the viewer reverts to its own sensible fixed cap
     instead of trying to fill a shared row height. */
  .remaster-viewfinder { height: 60vh; max-height: 60vh; min-height: 280px; flex: none; }
}

/* Video Upscale's Before/After — a simpler side-by-side pair of playable
   <video> elements rather than the draggable slider used for images above
   (dragging a slider across two independently-playing videos isn't a
   meaningful interaction the way it is for two static images), satisfying
   the same "video playback works after completion" requirement with far
   less complexity. Stacks to one column on narrow widths, same breakpoint
   as the rest of this page. */
.remaster-compare-video-row {
  position: absolute; inset: 0; display: flex; gap: 10px; padding: 10px; box-sizing: border-box;
}
.remaster-compare-video-col {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; align-items: stretch;
}
.remaster-compare-video-col video {
  width: 100%; flex: 1; min-height: 0; border-radius: 8px; background: #000; object-fit: contain;
}
.remaster-compare-label-static {
  font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.06em; color: var(--text-muted); text-align: center;
}
@media (max-width: 900px) {
  .remaster-compare-video-row { flex-direction: column; overflow-y: auto; }
  .remaster-compare-video-col video { min-height: 160px; }
}

/* Small, visually secondary product-clarification line under the page's
   main description — same class as the other .panel-sub lines, just
   deemphasized so it doesn't compete with them. */
.remaster-subtle-note {
  font-size: 0.75rem;
  color: var(--text-faint);
  max-width: 720px;
}

/* ---------- Studio Director drawer (Section 1: persistent memory + chat) ----------
   Deliberately reuses the same design tokens as every other panel/modal in
   this file (--surface-2, --border, --accent, --text, --text-muted) — no
   new colors introduced. Desktop: fixed right-side drawer. Mobile: bottom
   sheet (see the max-width:860px override below). */
.sd-backdrop {
  display: none;
  position: fixed; inset: 0; z-index: 1100;
  background: rgba(0, 0, 0, 0.45);
}
.sd-backdrop.open { display: block; }

/* ---------- Studio Agent — fail-CLOSED default (default-deny allowlist) ----------
   This rule is independent of, and stronger than, the sd-page-hidden
   blacklist immediately below it. By default — before any JS has run,
   during a route transition, on a slow/delayed session restore, or any
   other moment where the active page has not yet been POSITIVELY confirmed
   eligible — none of this chrome renders at all. #sd-rail-tab and
   .sd-mobile-launcher carry no "hidden" class of their own in the static
   index.html (only #sd-collapse-tab starts with one), so without this rule
   they are visible-by-default the instant the page paints, and only the
   later-arriving sd-page-hidden toggle below (added by sdApplyPageVisibility(),
   which only runs once JS has resolved the current view — see
   restoreViewFromHash()'s async auth/config wait in app.js) ever hid them.
   That gap between "HTML painted" and "JS decided" was the actual source of
   the intermittent Home flash — a fast connection or warm cache mostly hid
   it, a slow one didn't. sd-agent-eligible flips this to an allowlist: the
   body starts with NEITHER class, so this rule alone keeps every one of
   these elements out of the layout until sdApplyPageVisibility() explicitly
   proves the current view is a real, resolved, non-hidden workspace and adds
   sd-agent-eligible — never the other way around. */
body:not(.sd-agent-eligible) #sd-drawer,
body:not(.sd-agent-eligible) #sd-backdrop,
body:not(.sd-agent-eligible) #sd-rail-tab,
body:not(.sd-agent-eligible) #sd-collapse-tab,
body:not(.sd-agent-eligible) .sd-mobile-launcher {
  display: none !important;
}

/* ---------- Studio Agent — centralized page-visibility gate ----------
   body.sd-page-hidden is toggled by sdApplyPageVisibility() in app.js
   (driven by the one shared STUDIO_AGENT_HIDDEN_PAGES list — same rule for
   desktop and mobile, no separate hidden-page lists, no per-page CSS). The
   class lives on <body>, not #app-shell: #sd-drawer, #sd-backdrop,
   #sd-rail-tab, #sd-collapse-tab, and .sd-mobile-launcher are all mounted as
   *siblings* of #app-shell in index.html (after it closes), so a class on
   #app-shell could never reach them via a descendant selector — <body> is
   their actual nearest common ancestor. Forces the drawer, its backdrop,
   the desktop rail tab/collapse tab, and the mobile launcher all fully out
   of the layout — no dim, no partial state, no leftover edge tab/mobile
   launcher/bottom-sheet handle — while leaving every one of these elements'
   own .open/.hidden classes completely untouched underneath, so chat,
   project memory, and pending actions are never affected, and the exact
   same open-or-collapsed state reappears the instant the user returns to an
   allowed page. !important is needed here because it must win over the
   .open state each of these elements can independently carry. */
body.sd-page-hidden #sd-drawer,
body.sd-page-hidden #sd-backdrop,
body.sd-page-hidden #sd-rail-tab,
body.sd-page-hidden #sd-collapse-tab,
body.sd-page-hidden .sd-mobile-launcher {
  display: none !important;
}
/* Desktop's docked-layout push (#app-shell.sd-open's margin-right) is also
   cancelled while hidden — otherwise a hidden page would keep the
   workspace shrunk for a panel nothing can see, and the gap would only
   disappear once the (untouched) .sd-open state itself changed rather than
   the moment the page became allowed again. .sd-open itself stays on
   #app-shell (it's about #app-shell's own layout, unrelated to this
   sibling-element visibility gate) — this rule just also requires the
   body-level hidden flag to be set. */
body.sd-page-hidden #app-shell.sd-open {
  margin-right: 0;
}

/* Desktop: Studio Agent is a docked side workspace (see #app-shell.sd-open's
   margin-right push above), not a modal — the active page behind it must
   stay fully visible, normal-brightness, and clickable. The dark
   full-viewport backdrop (and its close-on-click handler sitting on top of
   the whole page) is a mobile/tablet-only concept for when the drawer is a
   bottom-sheet/overlay instead; on desktop it's forced off entirely,
   regardless of the .open class, rather than just left transparent, so it
   can never intercept clicks on the page underneath it either. */
@media (min-width: 861px) {
  .sd-backdrop,
  .sd-backdrop.open {
    display: none;
    pointer-events: none;
  }
}

.sd-drawer {
  position: fixed;
  /* Start below the top frame / shining border, not through it. Mobile
     overrides this to a bottom sheet (top:auto) further down. */
  top: var(--top-frame-height, 60px); right: 0; bottom: 0;
  width: 380px; max-width: 92vw;
  z-index: 1101;
  background: var(--surface-2);
  border-left: 1px solid var(--border);
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.28);
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.22s ease;
}
.sd-drawer.open { transform: translateX(0); }

/* Persistent right-edge launcher shown whenever the drawer is collapsed —
   desktop only (see the min-width: 861px block below); mobile/tablet keeps
   using the rail nav's existing Studio Director button instead. Toggled via
   a plain .hidden class in sdOpen()/sdClose(), same pattern as the rest of
   this app rather than a bespoke show/hide mechanism. */
.sd-rail-tab {
  display: none;
}
@media (min-width: 861px) {
  .sd-rail-tab {
    display: flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    top: 50%;
    right: 0;
    transform: translateY(-50%);
    width: 32px;
    height: 88px;
    font-size: 1.0625rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-right: none;
    border-radius: 12px 0 0 12px;
    box-shadow: -4px 0 12px rgba(0, 0, 0, 0.2);
    color: var(--text);
    cursor: pointer;
    z-index: 1090;
    transition: opacity 0.18s ease;
  }
  .sd-rail-tab:hover { opacity: 0.85; }
  .sd-rail-tab.hidden { display: none; }
}

/* Header: slightly taller, with real breathing room between the title and
   subtitle rather than a cramped two-line stack right under the top edge. */
.sd-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  padding: 14px 18px 13px;
  border-bottom: 1px solid var(--border-soft);
}
.sd-title { font-family: 'Archivo', sans-serif; font-weight: 800; font-size: 0.96875rem; color: var(--text); letter-spacing: 0.01em; }
.sd-subtitle { font-size: 0.71875rem; color: var(--text-faint); margin-top: 4px; }
/* Desktop collapse control lives as an edge tab (see .sd-collapse-tab
   below), not in the header. Mobile/tablet gets its own sticky in-header
   button instead (see .sd-mobile-collapse-btn right below) — the edge tab
   only makes sense next to a docked side drawer, and a bottom-sheet has no
   equivalent edge to anchor it to. */
.sd-mobile-collapse-btn {
  display: none;
}
@media (max-width: 860px) {
  .sd-mobile-collapse-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 34px;
    height: 34px;
    font-size: 0.9375rem;
    color: var(--text-muted);
    background: var(--surface-3);
    border: 1px solid var(--border-soft);
    border-radius: 999px;
    cursor: pointer;
  }
  .sd-mobile-collapse-btn:hover { color: var(--text); border-color: var(--accent); }
  /* .sd-head is already display:flex/space-between/align-items:flex-start
     (see .sd-head above) — this only needs to keep sitting on that same
     baseline as the title block next to it, which flex-start already does. */
}

/* Collapse tab: the exact same drawer-handle as .sd-rail-tab — same
   vertical center (top:50%/translateY(-50%)), height, width, border-radius,
   border weight, silver/graphite background, shadow, and leftward
   protrusion, all inherited unchanged by sharing that class. It only needs
   re-anchoring from the viewport's right edge to the open drawer's left
   edge (`right: 380px`, matching .sd-drawer's own width exactly, so the
   handle sits flush against the drawer instead of the screen edge) — the
   rounded-left/flush-right/shadow-cast-left shape already reads correctly
   in both spots since both protrude left of whatever they're attached to.
   Only the glyph differs (a left-pointing chevron for "collapse").
   Visibility is the mirror image of .sd-rail-tab — hidden while collapsed,
   shown while open — toggled by the same sdOpen()/sdClose() calls. */
@media (min-width: 861px) {
  .sd-collapse-tab {
    right: 380px;
  }
}

/* Project/chat controls row — compact chip buttons, each with its own
   small popover anchored directly beneath it. Deliberately NOT a modal:
   no backdrop, no page dimming/blur, and opening one never touches page or
   drawer scroll position (see the plain toggle-a-class JS in app.js) —
   requirement was that these stay lightweight dropdowns, not a screen. */
.sd-controls-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 10px 16px 0;
}
.sd-project-line {
  display: inline-flex;
  align-items: center;
  width: fit-content;
  padding: 5px 12px;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
  background: var(--surface-3);
  border: 1px solid var(--border-soft);
  border-radius: 999px;
  cursor: pointer;
  font-family: inherit;
}
.sd-project-line:hover { border-color: var(--accent); color: var(--text); }
.sd-chip-btn {
  display: inline-flex;
  align-items: center;
  width: fit-content;
  padding: 5px 12px;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
  background: var(--surface-3);
  border: 1px solid var(--border-soft);
  border-radius: 999px;
  cursor: pointer;
  font-family: inherit;
}
.sd-chip-btn:hover { border-color: var(--accent); color: var(--text); }
.sd-popover-wrap { position: relative; }
.sd-popover {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 200px;
  max-width: 280px;
  max-height: 260px;
  overflow-y: auto;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.35);
  padding: 6px;
  z-index: 40;
}
.sd-popover.hidden { display: none; }
.sd-popover-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 7px 10px;
  font-size: 0.75rem;
  color: var(--text);
  background: none;
  border: none;
  border-radius: 7px;
  cursor: pointer;
  font-family: inherit;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sd-popover-item:hover { background: var(--surface-3); }
.sd-popover-item.active { color: var(--accent); font-weight: 700; }
.sd-popover-divider { height: 1px; background: var(--border-soft); margin: 5px 2px; }
.sd-popover-empty { padding: 7px 10px; font-size: 0.71875rem; color: var(--text-faint); }

/* Inline cards rendered directly in the message stream (project creation,
   project-forming suggestion) — same bubble language as .sd-msg so they
   read as part of the conversation, not a separate overlay. */
.sd-inline-card {
  margin: 4px 0 14px;
  padding: 12px 14px;
  background: var(--surface-3);
  border: 1px solid var(--border-soft);
  border-radius: 12px;
  max-width: 90%;
}
.sd-inline-card-title { font-size: 0.75rem; font-weight: 700; color: var(--text); margin-bottom: 8px; }
.sd-inline-card label { display: block; font-size: 0.65625rem; font-weight: 600; color: var(--text-faint); margin: 8px 0 3px; }
.sd-inline-card input[type="text"], .sd-inline-card textarea {
  width: 100%;
  box-sizing: border-box;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: 7px;
  color: var(--text);
  font-size: 0.75rem;
  font-family: inherit;
  padding: 6px 9px;
  resize: vertical;
}
.sd-inline-card-actions { display: flex; gap: 8px; margin-top: 10px; }
.sd-inline-card-actions button {
  flex: 1;
  padding: 7px 10px;
  font-size: 0.71875rem;
  font-weight: 700;
  border-radius: 7px;
  cursor: pointer;
  font-family: inherit;
  border: 1px solid var(--border-soft);
}
.sd-inline-card-primary { background: var(--accent); color: #fff; border-color: var(--accent) !important; }
.sd-inline-card-secondary { background: var(--surface-2); color: var(--text-muted); }
.sd-inline-card-actions button:disabled { opacity: 0.6; cursor: default; }

/* Conversation area: a visually quieter background than the bubbles
   themselves, with generous inner padding and a reserved right-side gutter
   so the scrollbar never sits on top of message text. */
.sd-messages {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 20px 10px 20px 18px;
  margin-top: 12px;
  background: var(--surface);
  display: flex;
  flex-direction: column;
  /* Generous vertical gap is what actually keeps a back-and-forth chat
     readable — too tight and consecutive bubbles visually fuse into one
     block, which is what "bunched up / overlapping" was describing. */
  gap: 20px;
  scrollbar-gutter: stable;
}
.sd-msg {
  max-width: 76%;
  width: fit-content;
  padding: 11px 15px;
  /* Reserve room on the right for the scrollbar gutter above without
     letting the widest bubbles butt up against it. */
  margin-right: 6px;
  border-radius: 16px;
  font-size: 0.9375rem;
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: normal;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Paragraph/list structure inside a message (see sdRenderMessage in
   app.js, which now splits long assistant replies into <p>/<ol>/<ul>
   instead of one dense pre-wrapped blob) — purely presentational, the
   underlying message text is unchanged. */
.sd-msg p { margin: 0 0 12px; }
.sd-msg p:last-child { margin-bottom: 0; }
.sd-msg ol, .sd-msg ul {
  margin: 0 0 12px;
  padding-left: 22px;
}
.sd-msg ol:last-child, .sd-msg ul:last-child { margin-bottom: 0; }
.sd-msg li { margin-bottom: 6px; }
.sd-msg li:last-child { margin-bottom: 0; }
/* Resolved @reference tags inside a chat bubble (see sdMarkResolvedTags()
   in app.js) — same restrained, monochrome "stand out through weight, not
   color" language as .tag-highlight-overlay mark elsewhere in this file:
   stronger weight and a slightly stronger neutral tone, never a bright/
   neon browser-default <mark> highlight. Bumped up (rather than reusing
   .tag-highlight-overlay mark's own accent-color rule verbatim) because a
   chat bubble's background isn't the same neutral surface a prompt
   textarea sits on — .user's near-black background and .assistant's light
   surface both need their own readable contrast, not one shared color. */
.sd-msg mark {
  background: none; font: inherit; font-weight: 700;
}
.sd-msg.user mark { color: #ffffff; }
.sd-msg.assistant mark, .sd-msg.error mark { color: var(--text-primary-black, #111); }
.sd-msg.user {
  align-self: flex-end;
  background: var(--pure-black, #0a0a0a);
  color: #ffffff;
  border-bottom-right-radius: 4px;
}
.sd-msg.assistant {
  align-self: flex-start;
  background: var(--silver-polished);
  color: var(--text-primary-black, #111);
  border: 1px solid var(--border-soft);
  border-bottom-left-radius: 4px;
}
.sd-msg.error {
  align-self: flex-start;
  background: var(--silver-polished);
  color: var(--text-muted);
  border: 1px solid var(--border-soft);
  border-bottom-left-radius: 4px;
  font-style: italic;
}
/* Section 3 — the paid-action confirmation card. Deliberately distinct from
   a normal assistant bubble — compact approval-card layout with a real,
   high-contrast filled button — so it reads as "this needs your decision,"
   never as ordinary chat text a user could mistake for something
   already-done or skim past. Redesigned to be small and glanceable: a
   status label, the action title, the model, one compact metadata line,
   then a full-width primary CTA with a small, visually quiet Cancel
   underneath — not the old wide pill-row + two-equal-buttons layout, which
   read as a block of text with the button buried at the bottom. */
.sd-confirm-card {
  max-width: 78%;
  width: 260px;
  background: var(--pure-black, #0a0a0a);
  color: #fff;
  border: 1px solid var(--border-soft);
  border-bottom-left-radius: 4px;
  padding: 14px 14px 12px;
}
.sd-confirm-status {
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
  margin-bottom: 4px;
}
.sd-confirm-title {
  font-size: 0.9375rem;
  font-weight: 700;
  color: #fff;
  margin-bottom: 2px;
}
.sd-confirm-model {
  font-size: 0.8125rem;
  color: rgba(255, 255, 255, 0.75);
  margin-bottom: 2px;
}
.sd-confirm-meta {
  font-size: 0.75rem;
  color: rgba(255, 255, 255, 0.5);
  margin-bottom: 12px;
}
.sd-confirm-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.sd-confirm-go {
  width: 100%;
  background: #fff;
  color: #0a0a0a;
  border: 1px solid transparent;
  border-radius: 10px;
  padding: 11px 12px;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: background 0.15s ease, transform 0.05s ease;
}
.sd-confirm-go:hover:not(:disabled) { background: rgba(255, 255, 255, 0.85); }
.sd-confirm-go:active:not(:disabled) { transform: scale(0.98); }
.sd-confirm-go:disabled { opacity: 0.6; cursor: default; }
.sd-confirm-cancel {
  width: auto;
  background: transparent;
  color: rgba(255, 255, 255, 0.55);
  border: none;
  padding: 4px 8px;
  font-size: 0.75rem;
  font-weight: 500;
  cursor: pointer;
}
.sd-confirm-cancel:hover:not(:disabled) { color: rgba(255, 255, 255, 0.85); }
.sd-confirm-cancel:disabled { opacity: 0.6; cursor: default; }
.sd-empty-hint {
  color: var(--text-faint);
  font-size: 0.8125rem;
  line-height: 1.6;
  text-align: center;
  padding: 0 18px;
  margin-top: 28px;
}

.sd-thinking {
  padding: 6px 18px 12px;
  font-size: 0.71875rem;
  color: var(--text-faint);
  font-style: italic;
}

/* Composer: its own fixed visual zone, clearly divided from the last
   message rather than looking attached to it. */
.sd-input-row {
  display: flex; gap: 10px;
  align-items: center;
  padding: 12px 16px;
  border-top: 1px solid var(--border-soft);
  background: var(--surface-2);
}
.sd-input-row input {
  flex: 1;
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 10px 14px;
  font-size: 0.875rem;
  height: 38px;
  box-sizing: border-box;
}
.sd-input-row input:focus { outline: none; border-color: var(--accent); }
/* Wraps #sd-input so its @ reference autocomplete (see
   setupTagAutocompleteWithThumbs() in app.js, the SAME shared function
   Image/Video use — never a separate Studio Agent-only implementation) has
   a position:relative anchor to render against, exactly like Image/Video's
   own .tag-textarea-wrap. The wrapper — not the input — is the actual flex
   item now, so it carries the flex:1 that used to live directly on
   .sd-input-row input above; the input itself just fills the wrapper at
   width:100%, keeping its own visual size/style completely unchanged.
   .tag-autocomplete's existing CSS (absolute, bottom: calc(100% + 4px)) is
   fully generic — reused as-is, no Studio-Agent-specific autocomplete
   styling needed, including on mobile (rows stay the same 24px-thumbnail,
   touch-sized layout, and the dropdown's left:0/right:0 already keeps it
   from overflowing the narrower mobile bottom-sheet width). */
.sd-input-tagwrap { position: relative; flex: 1; min-width: 0; }
.sd-input-tagwrap input { width: 100%; }
.sd-send-btn {
  background: var(--accent);
  color: var(--silver-bright);
  border: none;
  border-radius: var(--radius);
  padding: 10px 16px;
  height: 38px;
  box-sizing: border-box;
  font-size: 0.84375rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.sd-send-btn:hover { opacity: 0.9; }
.sd-send-btn:disabled { opacity: 0.5; cursor: default; }

/* Composer image attachments — kept compact per spec: a small round attach
   button beside the input, and a thin thumbnail strip above the input row
   that only appears once something is actually attached (never an
   always-visible empty media panel). */
.sd-attach-btn {
  flex-shrink: 0;
  width: 38px; height: 38px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface-3);
  color: var(--text-muted);
  font-size: 1rem;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.sd-attach-btn:hover { border-color: var(--accent); color: var(--text); }
.sd-attach-strip {
  display: flex; gap: 8px; flex-wrap: wrap;
  padding: 10px 16px 0;
  background: var(--surface-2);
}
.sd-attach-thumb {
  position: relative;
  width: 48px; height: 48px;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--border);
}
.sd-attach-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sd-attach-remove {
  position: absolute; top: 2px; right: 2px;
  width: 16px; height: 16px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.65);
  color: #fff;
  font-size: 0.625rem;
  line-height: 1;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.sd-input-row.sd-dragover { outline: 2px dashed var(--accent); outline-offset: -2px; }

/* A sent image attachment inside a chat bubble — small, inline, never a
   full-bleed media block (keeps the bubble reading as chat, not a gallery). */
.sd-msg-attachments { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
.sd-msg-attachments img {
  width: 96px; height: 96px; object-fit: cover;
  border-radius: 10px; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.15);
}

@media (max-width: 860px) {
  .sd-drawer {
    top: auto;
    left: 0; right: 0; bottom: 0;
    width: auto; max-width: none;
    /* dvh tracks the actual visible viewport (browser chrome included/
       excluded as it really is right now) — layered on top of the vh
       fallback exactly like the Library modal fix above, for the same
       reason: a fixed vh sheet can end up taller or shorter than what's
       actually on screen depending on address-bar state. */
    height: 72vh;
    height: 72dvh;
    border-left: none;
    border-top: 1px solid var(--border);
    border-radius: 16px 16px 0 0;
    transform: translateY(100%);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .sd-drawer.open { transform: translateY(0); }
}

/* Mobile/tablet-only persistent launcher — see the HTML comment above
   #sd-mobile-launcher. Hidden entirely on desktop, where the existing
   right-edge .sd-rail-tab already serves this exact purpose; visibility
   is toggled by the same sdOpen()/sdClose() calls that toggle sd-rail-tab
   (both share the "hidden" class), so the two never show at once. */
.sd-mobile-launcher {
  display: none;
}
@media (max-width: 860px) {
  .sd-mobile-launcher {
    display: flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    right: 16px;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    width: 52px;
    height: 52px;
    border-radius: 999px;
    font-size: 1.375rem;
    background: var(--surface-2);
    border: 1px solid var(--border);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3);
    z-index: 1099;
  }
  .sd-mobile-launcher.hidden { display: none; }
}


/* ============================================================
   MOODBOARDS
   ============================================================ */
.moodboards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; }

.moodboard-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  overflow: hidden;
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.moodboard-card:hover { border-color: var(--accent); transform: translateY(-2px); }

.moodboard-card-collage {
  aspect-ratio: 4 / 3;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 2px;
  background: var(--surface-2);
}
.moodboard-card-collage-cell { background-size: cover; background-position: center; background-color: var(--surface-2); }
.moodboard-card-collage-cell.empty { display: flex; align-items: center; justify-content: center; color: var(--text-faint); font-size: 1.25rem; }
.moodboard-card-collage.single .moodboard-card-collage-cell { grid-column: 1 / -1; grid-row: 1 / -1; }

.moodboard-card-body { padding: 10px 12px 12px; display: flex; flex-direction: column; gap: 6px; }
.moodboard-card-name-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.moodboard-card-name { font-family: var(--font-display); font-weight: 700; font-size: 0.875rem; line-height: 1.3; word-break: break-word; }
.moodboard-card-count { font-family: var(--font-mono); font-size: 0.6875rem; color: var(--text-faint); }
.moodboard-card-badges { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

.moodboard-active-badge {
  font-size: 0.625rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: #0a0a0b; background: var(--accent); padding: 3px 8px; border-radius: 999px;
}

.moodboard-status-pill {
  font-size: 0.625rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 999px; display: inline-flex; align-items: center; gap: 5px;
}
.moodboard-status-pill.needs_more_images { background: var(--accent-soft); color: var(--accent); }
.moodboard-status-pill.analyzing { background: var(--accent-soft); color: var(--accent); }
.moodboard-status-pill.ready { background: var(--success-soft); color: var(--success); }
.moodboard-status-pill.analysis_failed { background: var(--error-soft); color: var(--error); }
.moodboard-status-pill-retry {
  border: none; background: none; color: inherit; text-decoration: underline;
  cursor: pointer; font: inherit; padding: 0; text-transform: none; letter-spacing: normal;
}

.moodboard-card-menu-btn {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 26px; height: 26px; border-radius: 999px;
  background: rgba(20, 20, 23, 0.65); border: 1px solid var(--border); color: #fff;
  display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 0.875rem; line-height: 1;
}
.moodboard-card-menu-btn:hover { background: var(--surface-3); border-color: var(--accent); }

.moodboard-card-menu {
  position: absolute; top: 38px; right: 8px; z-index: 5;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  min-width: 140px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); overflow: hidden;
}
.moodboard-card-menu button {
  display: block; width: 100%; text-align: left; padding: 9px 14px;
  background: none; border: none; color: var(--text); font: inherit; font-size: 0.8125rem; cursor: pointer;
}
.moodboard-card-menu button:hover { background: var(--surface-3); }
.moodboard-card-menu button.danger { color: var(--danger); }

/* Editor modal */
.moodboard-editor-card { max-width: 640px; width: 92vw; }
.moodboard-editor-head-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.moodboard-name-input { flex: 1; }
.moodboard-image-counter { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-faint); white-space: nowrap; }

.moodboard-status-banner {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px; border-radius: 10px; margin-bottom: 14px;
  font-size: 0.8125rem; font-weight: 600;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-muted);
}
.moodboard-status-banner.ready { background: var(--success-soft); color: var(--success); border-color: transparent; }
.moodboard-status-banner.analyzing { background: var(--accent-soft); color: var(--accent); border-color: transparent; }
.moodboard-status-banner.analysis_failed { background: var(--error-soft); color: var(--error); border-color: transparent; }
.moodboard-status-banner-retry {
  margin-left: auto; border: 1px solid currentColor; background: none; color: inherit;
  padding: 5px 12px; border-radius: 999px; font: inherit; font-size: 0.75rem; cursor: pointer;
}

.moodboard-editor-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 10px;
  max-height: 320px; overflow-y: auto; margin-bottom: 14px;
}
.moodboard-editor-thumb { position: relative; aspect-ratio: 1; border-radius: 8px; overflow: hidden; background: var(--surface-2); }
.moodboard-editor-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.moodboard-editor-thumb-remove {
  position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; border-radius: 999px;
  background: rgba(0,0,0,0.65); color: #fff; border: none; display: flex; align-items: center; justify-content: center;
  cursor: pointer; font-size: 0.75rem; line-height: 1;
}
.moodboard-editor-thumb-remove:hover { background: var(--danger); }

.moodboard-editor-add-row { display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }
.moodboard-editor-add-row .secondary-btn { flex: 1; min-width: 160px; }

/* Image page moodboard selector */
.moodboard-selector { position: relative; }
.moodboard-selector-btn {
  width: 100%; text-align: left; padding: 10px 14px; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  font-family: var(--font-mono); font-size: 0.78125rem; font-weight: 600; letter-spacing: 0.02em;
  cursor: pointer;
}
.moodboard-selector-btn:hover { border-color: var(--accent); }
.moodboard-selector-btn.active-state { color: var(--accent); }
/* NONE is a valid, deliberate choice — not an error/warning state — so it
   stays the button's normal bold/dark text color (the default above) rather
   than any red/danger treatment. .none-state exists as an explicit hook in
   case NONE ever needs its own look, but currently just confirms the
   default already satisfies "bold, dark graphite, no warning color". */
.moodboard-selector-btn.none-state { color: var(--text); font-weight: 700; }
.moodboard-selector-menu {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 8;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.25); overflow: hidden; max-height: 260px; overflow-y: auto;
}
.moodboard-selector-menu button {
  display: block; width: 100%; text-align: left; padding: 10px 14px;
  background: none; border: none; color: var(--text); font: inherit; font-size: 0.8125rem; cursor: pointer;
  border-bottom: 1px solid var(--border);
}
.moodboard-selector-menu button:last-child { border-bottom: none; }
.moodboard-selector-menu button:hover { background: var(--surface-3); }
.moodboard-selector-menu .moodboard-selector-empty { color: var(--text-faint); cursor: default; }
/* NONE in the dropdown is the same valid, non-destructive choice as above —
   bold/dark, never red/danger (previously this was labeled "Turn off
   Moodboard" and styled with var(--danger); renamed + recolored together). */
.moodboard-selector-menu .moodboard-selector-none { font-weight: 700; color: var(--text); }
.moodboard-selector-menu button.active { color: var(--accent); font-weight: 700; }

/* ============================================================
   PHASE 8 — Onboarding (welcome card + quick tour). Reuses the
   existing .modal-overlay/.modal-card family so it inherits Phase 7's
   mobile hardening for free — only tour-specific bits live here.
   ============================================================ */
.onboarding-steps {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 16px;
}
.onboarding-step-dots {
  display: flex;
  gap: 6px;
  justify-content: center;
  margin: 4px 0 14px;
}
.onboarding-step-dot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--border);
  transition: background 0.15s ease, transform 0.15s ease;
}
.onboarding-step-dot.active {
  background: var(--accent, #7c5cff);
  transform: scale(1.25);
}
.onboarding-step-icon {
  font-size: 1.625rem;
  margin-bottom: 6px;
}
.onboarding-step-title {
  font-size: 0.9375rem;
  font-weight: 700;
  margin-bottom: 4px;
}
.onboarding-step-body {
  font-size: 0.8125rem;
  color: var(--text-muted);
  line-height: 1.5;
}
.onboarding-tour-nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  margin-top: 16px;
}
.onboarding-tour-nav-right {
  display: flex;
  gap: 8px;
}
.onboarding-skip-link {
  background: none;
  border: none;
  color: var(--text-faint);
  font-size: 0.75rem;
  cursor: pointer;
  padding: 6px 4px;
}
.onboarding-skip-link:hover { color: var(--text-muted); }

/* Small "?" contextual help affordance (Section 12) — unobtrusive, never
   covers page content; opens a tiny inline popover with one or two
   sentences on how the page fits the workflow. */
.help-hint-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-3);
  color: var(--text-faint);
  font-size: 0.6875rem;
  line-height: 1;
  cursor: pointer;
  margin-left: 6px;
  vertical-align: middle;
}
.help-hint-btn:hover { color: var(--text); border-color: var(--accent, #7c5cff); }
.help-hint-popover {
  position: absolute;
  z-index: 500;
  max-width: 260px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  font-size: 0.75rem;
  color: var(--text-muted);
  line-height: 1.45;
  box-shadow: 0 8px 24px rgba(0,0,0,0.35);
  display: none;
}
.help-hint-popover.open { display: block; }

@media (max-width: 640px) {
  .onboarding-step-icon { font-size: 1.375rem; }
  .onboarding-tour-nav { flex-wrap: wrap; }
}

/* ============================================================
   PHASE 9 — Admin Analytics + Margin Visibility. Reuses
   .admin-stat-card/.admin-users-table's look so the new panel doesn't
   redesign the Admin page (Section 58: mobile-safe — cards wrap, tables
   scroll locally, never the page itself).
   ============================================================ */
.admin-analytics-panel { margin-bottom: 24px; }
.admin-analytics-cost-basis {
  font-size: 0.6875rem;
  color: var(--text-faint);
  cursor: help;
}
.admin-analytics-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 12px 0 18px;
}
.admin-analytics-filters select,
.admin-analytics-filters input[type="date"],
.admin-analytics-filters input[type="text"] {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 8px 10px;
  font-size: 0.78125rem;
}
.admin-analytics-filters input[type="text"] { min-width: 180px; }

.admin-analytics-cards {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
  margin-bottom: 20px;
}
@media (max-width: 1300px) { .admin-analytics-cards { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .admin-analytics-cards { grid-template-columns: 1fr; } }

.admin-analytics-charts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
  margin-bottom: 24px;
}
@media (max-width: 1100px) { .admin-analytics-charts { grid-template-columns: 1fr; } }

.analytics-chart {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 14px;
}
.analytics-chart-title {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 12px;
}
.analytics-chart-bars {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 120px;
  overflow-x: auto;
}
.analytics-bar-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  min-width: 14px;
  flex: 1 0 auto;
  height: 100%;
}
.analytics-bar {
  width: 100%;
  max-width: 18px;
  background: var(--accent, #7c5cff);
  border-radius: 3px 3px 0 0;
  min-height: 2px;
}
.analytics-bar-label {
  font-size: 0.53125rem;
  color: var(--text-faint);
  margin-top: 4px;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  max-height: 40px;
}

.admin-analytics-subsection { margin-bottom: 24px; }
.admin-analytics-grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}
@media (max-width: 1000px) { .admin-analytics-grid-2 { grid-template-columns: 1fr; } }

.admin-analytics-table-wrap { overflow-x: auto; margin-top: 10px; }
.admin-analytics-table { width: 100%; border-collapse: collapse; font-size: 0.78125rem; }
.admin-analytics-table th {
  text-align: left; padding: 9px 12px; color: var(--text-faint); font-weight: 700;
  font-family: var(--font-mono); font-size: 0.65625rem; letter-spacing: 0.4px; text-transform: uppercase;
  border-bottom: 1px solid var(--border-soft);
  cursor: pointer;
  white-space: nowrap;
}
.admin-analytics-table th:hover { color: var(--text); }
.admin-analytics-table td { padding: 11px 12px; border-bottom: 1px solid var(--border-soft); color: var(--text); white-space: nowrap; }
.admin-analytics-table tbody tr:hover { background: var(--surface-2); }
.admin-analytics-table tbody tr:last-child td { border-bottom: none; }
.table-subtext { font-size: 0.65625rem; color: var(--text-faint); white-space: nowrap; }

.margin-pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 700;
  font-family: var(--font-mono);
}
.margin-ok { background: rgba(60, 200, 130, 0.14); color: #3cc882; }
.margin-low { background: rgba(230, 180, 40, 0.16); color: #e6b428; }
.margin-negative { background: rgba(230, 70, 70, 0.16); color: #e64646; }
.margin-na { background: var(--surface-3); color: var(--text-faint); }

.admin-job-status-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.admin-mini-stat {
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  padding: 8px 12px;
  min-width: 84px;
  text-align: center;
}
.admin-mini-stat-num { font-family: var(--font-mono); font-weight: 800; font-size: 1rem; color: var(--accent); }
.admin-mini-stat-label { font-size: 0.625rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.3px; margin-top: 2px; }

/* Mobile Admin containment. Desktop above is untouched — flex-wrap mini
   stat cards and full-width tables both work fine at desktop widths. On
   narrow viewports two things were spilling past the right edge: (1) flex
   items have an implicit min-width:auto, so a long uppercase label like
   "VALIDATION FAILED" could force a card (and its .admin-job-status-row
   parent, and everything up the ancestor chain lacking its own
   min-width:0) wider than the viewport instead of wrapping; (2) the wide
   analytics tables (Plan Profitability, One-Time Credit Packs, Provider
   Breakdown) already sit in a horizontally-scrollable .admin-analytics-
   table-wrap, which is correct, but nothing constrained the grid cell
   containing that wrapper, so the wrapper itself could still stretch the
   page. Fixing both with min-width:0 + a 2-column grid for the mini stat
   rows keeps the outer Admin page scrolling vertically only, while each
   wide table keeps its own local horizontal scroll. */
@media (max-width: 640px) {
  .admin-analytics-subsection,
  .admin-analytics-grid-2,
  .admin-analytics-grid-2 > div,
  .admin-analytics-table-wrap {
    min-width: 0;
    max-width: 100%;
    box-sizing: border-box;
  }
  .admin-job-status-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
  }
  .admin-mini-stat {
    min-width: 0;
    width: 100%;
    box-sizing: border-box;
    overflow-wrap: anywhere;
  }
  .admin-mini-stat-label { white-space: normal; }
}

.admin-stuck-jobs-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.admin-stuck-job-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: rgba(230, 180, 40, 0.08);
  border: 1px solid rgba(230, 180, 40, 0.25);
  border-radius: var(--radius);
  font-size: 0.75rem;
  flex-wrap: wrap;
}
.admin-stuck-job-status {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.65625rem;
  text-transform: uppercase;
  color: #e6b428;
}

.admin-stuck-job-row { justify-content: space-between; }
.admin-stuck-job-reconcile {
  margin-left: auto;
  background: rgba(230, 180, 40, 0.15);
  border: 1px solid rgba(230, 180, 40, 0.4);
  color: #e6b428;
  border-radius: var(--radius);
  padding: 4px 10px;
  font-size: 0.6875rem;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
}
.admin-stuck-job-reconcile:hover { background: rgba(230, 180, 40, 0.25); }
.admin-stuck-job-reconcile:disabled { opacity: 0.6; cursor: default; }

.admin-analytics-footnote {
  font-size: 0.6875rem;
  color: var(--text-faint);
  line-height: 1.5;
  border-top: 1px solid var(--border-soft);
  padding-top: 12px;
}

@media (max-width: 640px) {
  .admin-analytics-filters { flex-direction: column; align-items: stretch; }
  .admin-analytics-filters select,
  .admin-analytics-filters input { width: 100%; }
}

/* ============================================================
   PHASE 10 — Favorites / Collections organization layer.
   Monochrome favorite glyph (Section 2 — no bright social-media styling),
   reuses .entity-card/.icon-btn/.modal-overlay/.card-grid/.projects-grid
   throughout rather than introducing a parallel visual system.
   ============================================================ */
.favorite-btn.is-favorited {
  color: var(--text);
  border-color: var(--text-faint);
}
.favorite-btn.is-favorited:hover { color: var(--danger, #e64646); }

.collection-card { position: relative; cursor: pointer; }
.collection-card-overlay {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 10px 12px;
  background: linear-gradient(to top, rgba(0,0,0,0.75), rgba(0,0,0,0));
  color: #fff;
}
.collection-card-name { font-size: 0.8125rem; font-weight: 700; }
.collection-card-count { font-size: 0.6875rem; opacity: 0.8; margin-top: 2px; }

.collection-detail-head { margin-bottom: 18px; }
.collection-detail-title-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-top: 12px;
}
.collection-detail-title-row h2 { margin: 0; }
.collection-detail-count { font-size: 0.75rem; color: var(--text-faint); }
.collection-detail-actions { display: flex; gap: 10px; margin-top: 10px; }
.secondary-btn.danger-btn:hover { border-color: var(--danger, #e64646); color: var(--danger, #e64646); }

.add-to-collection-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 240px;
  overflow-y: auto;
  margin: 12px 0;
}
.add-to-collection-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius);
  cursor: pointer;
  font-size: 0.8125rem;
}
.add-to-collection-row:hover { background: var(--surface-2); }
.add-to-collection-row input[type="checkbox"] { width: 16px; height: 16px; }
.add-to-collection-new-row { display: flex; gap: 8px; margin-bottom: 4px; }
.add-to-collection-new-row .modal-input { flex: 1; margin: 0; }

@media (max-width: 640px) {
  .add-to-collection-new-row { flex-direction: column; }
}

/* ============================================================
   AVATAR page
   ============================================================
   Additive only. The page reuses the Video console's silver-dotted
   workspace classes verbatim (.video-console-grid / .video-console-bottom,
   including their 1100px and 860px behaviour and the mobile "Generate is
   the last control before History" ordering), so everything below is
   strictly the Avatar-specific delta — never a second copy of that
   system, and never anything from the Editor's black workspace. */

/* CENTER THE AVATAR COLUMN IN THE WORKSPACE.
   Every other generation page carries a `main:has(> #view-X.active)` rule
   (Image, Video, Scene, Storyboard, Shot Extractor, Upscale, Characters,
   Character Sheet — see them throughout this file). Avatar was added later
   and never got one, so it fell through to the bare `main` defaults:
   `max-width: 1180px` with NO auto margins. A grid item narrower than its
   track and without auto margins sits flush against the start edge — so on
   any viewport wider than ~1480px the whole Avatar page hugged the sidebar
   with all the slack piling up on the right. Measured at a 1920px viewport
   before this rule: 484px of dead space to the right of the content.

   The fix is deliberately just the centring. `margin-inline: auto` centres
   the existing 1180px column inside the post-sidebar workspace and changes
   nothing else — not the width, not the padding, not the background, not
   the console's own internal layout. The output viewer and the settings
   card are already siblings in one flex column (.avatar-console-grid), so
   centring their shared container is what puts them on a common centre
   line; neither needed touching individually.

   CORRECTION: Avatar now takes the same full-bleed dotted workspace every
   other generation page uses, which was explicitly requested. This also
   replaces the earlier `margin-inline: auto` centring, and is a better fix
   than that was.

   Centring main itself meant main stayed 1180px wide and simply sat in the
   middle of the viewport, so everything outside it — most of the screen —
   was bare silver with no dots, and the page header (Back button included)
   started wherever that floating column happened to begin rather than at the
   left edge of the workspace.

   The shared pattern separates the two concerns properly: MAIN goes
   full-bleed and carries the dots, and the VIEW inside it is the centred,
   width-constrained column. Content ends up in exactly the same place and at
   exactly the same width as before (1180px cap less 44px padding = the same
   1092px content box), the dots now reach both edges, and the Back button
   lands at the left edge of the content column like every other page.

   Dots are folded into main's own background rather than a ::before, so the
   pattern tiles the full scrollable height instead of stopping at the first
   viewport — same reasoning as #view-explore's fix further up. */
main:has(> #view-avatar.active) {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  padding: 0;
  position: relative;
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 1px, transparent 1px, transparent 3px),
    repeating-linear-gradient(180deg, rgba(10, 10, 10, 0.035) 0px, transparent 40px, rgba(255, 255, 255, 0.05) 80px, transparent 120px),
    var(--surface-gradient);
  background-size: 22px 22px, auto, auto, auto;
}
#view-avatar.active {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  /* main's own former padding/cap, moved in here so the content column keeps
     the exact width and inset it had before this change. */
  padding: 36px 44px 60px;
  max-width: 1180px;
  margin-left: auto;
  margin-right: auto;
}

/* The Video console's bottom panel is a two-column grid (control + flyout).
   Avatar has a single control column, so it collapses to one full-width
   track — otherwise the lone column would sit in a 1.3fr slot with dead
   space beside it. */
.avatar-console-bottom { grid-template-columns: minmax(0, 1fr); }

/* Compact read-only model/provider line (there is one model at launch, so
   no selector). Styled as a quiet status row, not a disabled control. */
.avatar-model-status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.avatar-model-status .section-hint { margin-top: 2px; }

/* Remove / Replace pair under the source-image and audio slots. */
.avatar-slot-actions {
  display: flex;
  gap: 14px;
  margin-top: 8px;
  flex-wrap: wrap;
}

/* Voice speed / pitch sliders. */
.avatar-slider-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 10px;
  flex-wrap: wrap;
}
.avatar-slider-row > label {
  flex: 0 0 auto;
  min-width: 108px;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted, #5a5a5a);
}
.avatar-slider-row > input[type="range"] { flex: 1 1 160px; min-width: 0; }

.avatar-bg-color-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 12px;
  flex-wrap: wrap;
}
.avatar-bg-color-row input[type="color"] {
  width: 46px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: none;
  cursor: pointer;
}

/* Audio-mode preview (dormant while the audio capability is off).
   The waveform is a REAL canvas rendering of decoded peaks — on narrow
   screens it draws fewer peaks, never a decorative flat line. */
.avatar-audio-preview { margin-top: 12px; }
.avatar-audio-meta {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  font-size: 0.75rem;
  color: var(--text-muted, #5a5a5a);
  margin-bottom: 6px;
}
.avatar-waveform {
  display: block;
  width: 100%;
  height: 56px;
  border: 1px solid var(--border-soft, #d5d5d5);
  border-radius: 6px;
  background: #f4f4f4;
}

/* The output viewer plays with sound, so it must never be muted by the
   silent-preview rules used for history thumbnails. */
.avatar-console-grid .viewfinder video { max-width: 100%; max-height: 100%; }

/* History rows: one compact metadata line under each Avatar card. */
.avatar-history-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  padding: 8px 10px 2px;
  font-size: 0.6875rem;
  letter-spacing: 0.03em;
  color: var(--text-muted, #5a5a5a);
}
.avatar-history-meta .avatar-history-ref { opacity: 0.75; }

/* Rail icon: inline line-art SVG, monochrome via currentColor so it picks
   up the rail's own black/silver states exactly like the glyph icons. */
.rail-icon-svg {
  width: 15px;
  height: 15px;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (max-width: 640px) {
  .avatar-slider-row > label { min-width: 90px; }
  .avatar-waveform { height: 44px; }
}

/* Mobile: stop a fast second tap being read as double-tap-to-zoom.
   Reported as the page "going left and right and expanding", needing a manual
   resize back. There is no horizontal overflow to pan to on any view - the pan
   is a consequence of the zoom, and the zoom comes from double-tap, which is
   easy to trigger by accident when tapping a control twice.

   touch-action: manipulation removes double-tap zoom while leaving pinch-zoom
   alone, so someone who genuinely wants to zoom in on an image still can.
   Deliberately NOT user-scalable=no, which takes that away from people who
   need it. */
@media (pointer: coarse) {
  body,
  button, a, label, select, summary,
  input[type="checkbox"], input[type="radio"], input[type="range"],
  .rail-btn, .primary-btn, .secondary-btn, .icon-btn,
  .project-hub-tile, .entity-card, .moodboard-selector-btn {
    touch-action: manipulation;
  }
}

/* --- Studio / Project hub dots on small screens ---------------------------
   The dotted layer was never disabled on mobile: the CSS is byte-identical at
   every width, the ::before spans the full scroll height (measured at 7694px
   on a 375px viewport), and no media query touches it. The dots are simply
   below the perceptual floor on a phone.

   The numbers: rgba(180,186,189,0.14) over #000 composites to rgb(25,26,26) —
   10% of white — in a 1px dot covering 0.65% of the area. At DPR 1 that is a
   crisp single pixel and reads fine. At the DPR 2-3 of a real phone the
   hard-stopped 1px dot is antialiased across 2-3 device pixels, which spreads
   that same 10% of luminance thinner and drops the peak below what an OLED
   shows at normal brightness. The pattern is being drawn; it just cannot be
   seen.

   So this restores the INTENDED appearance rather than inventing a second
   pattern: identical colour, identical 22px spacing, identical uniform
   coverage with no mask or fade. Only the dot radius and alpha are nudged, and
   only far enough to survive high-DPI resampling, so what a phone shows
   matches what a desktop shows. Desktop is untouched — this block is scoped to
   small viewports and adds nothing above 640px. */
@media (max-width: 640px) {
  #view-studio-hub::before,
  #view-project-hub::before {
    background-image: radial-gradient(rgba(180, 186, 189, 0.22) 1.4px, transparent 1.5px);
    /* Spacing deliberately unchanged, so the grid rhythm is the same one. */
    background-size: 22px 22px;
  }
}

/* =====================================================================
   STUDIO AGENT — ATTACHMENT SOURCE MENU + LIBRARY PICKER
   =====================================================================
   The paperclip offers two sources instead of jumping straight to the
   system file picker. Both the menu and the picker open IN PLACE over the
   chat — the user never leaves Studio Agent. */
.sd-attach-wrap { position: relative; display: flex; }

.sd-attach-menu {
  position: absolute; bottom: calc(100% + 8px); left: 0;
  z-index: 80; min-width: 210px; padding: 5px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 10px; box-shadow: 0 14px 34px -10px rgba(0, 0, 0, 0.45);
}
.sd-attach-menu.hidden { display: none; }
.sd-attach-menu-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 11px; border: 0; border-radius: 7px; cursor: pointer;
  background: transparent; color: var(--text); font: inherit; font-size: 0.8125rem; text-align: left;
}
.sd-attach-menu-item:hover { background: var(--surface-3); }
.sd-attach-menu-icon { width: 18px; text-align: center; flex-shrink: 0; }

.sd-lib-backdrop {
  position: fixed; inset: 0;
  /* Above the Studio Agent drawer (z-index 1101), which the picker is
     launched from and must cover; still below the lightbox family (10000+). */
  z-index: 1200;
  display: flex; align-items: center; justify-content: center;
  padding: 24px; background: rgba(0, 0, 0, 0.55);
}
.sd-lib-backdrop.hidden { display: none; }
.sd-lib {
  display: flex; flex-direction: column;
  width: min(860px, 100%); height: min(760px, 88vh); max-height: 88vh;
  background: var(--surface-1, var(--surface-2)); color: var(--text);
  border: 1px solid var(--border); border-radius: 14px; overflow: hidden;
}
.sd-lib-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 13px 16px; border-bottom: 1px solid var(--border);
}
.sd-lib-title { font-weight: 700; font-size: 0.875rem; }
.sd-lib-close {
  width: 30px; height: 30px; padding: 0; cursor: pointer; font-size: 0.875rem;
  background: transparent; border: 1px solid transparent; border-radius: 7px; color: var(--muted);
}
.sd-lib-close:hover { color: var(--text); border-color: var(--border); }

.sd-lib-controls {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 11px 16px; border-bottom: 1px solid var(--border);
}
.sd-lib-scopes { display: flex; gap: 5px; }
.sd-lib-scope {
  padding: 7px 12px; cursor: pointer; font: inherit; font-size: 0.75rem;
  background: transparent; color: var(--muted);
  border: 1px solid var(--border); border-radius: 7px;
}
.sd-lib-scope.active { background: var(--surface-3); color: var(--text); border-color: var(--border-strong, var(--border)); }
.sd-lib-search {
  flex: 1 1 160px; min-width: 0; padding: 8px 11px; font: inherit; font-size: 0.8125rem;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
}

.sd-lib-grid {
  flex: 1 1 auto; overflow-y: auto; overflow-x: hidden;
  min-height: 0;
  padding: 14px 16px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: max-content;
  gap: 14px;
  align-content: start;
}
.sd-lib-empty { grid-column: 1 / -1; padding: 30px 4px; text-align: center; color: var(--muted); font-size: 0.8125rem; }

.sd-lib-item {
  /* Flex column so the thumb + meta stack reliably and the card can never
     collapse to a thin strip; the thumb holds its square via aspect-ratio. */
  position: relative; display: flex; flex-direction: column; padding: 0; width: 100%; min-width: 0;
  cursor: pointer; text-align: left; font: inherit; color: inherit;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 10px; overflow: hidden;
}
.sd-lib-item:hover { border-color: var(--border-strong, var(--muted)); }
.sd-lib-item.selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent) inset; }
/* Square via aspect-ratio; flex: none keeps the grid row from squashing it. */
.sd-lib-thumb {
  position: relative; display: block; flex: none;
  width: 100%; aspect-ratio: 1 / 1;
  background: var(--surface-3); overflow: hidden;
}
.sd-lib-thumb img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; max-width: 100%; max-height: 100%;
  object-fit: cover; display: block;
}
.sd-lib-meta { display: block; padding: 8px 9px 9px; }
.sd-lib-name {
  display: block; font-size: 0.75rem; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sd-lib-sub { display: block; margin-top: 2px; font-size: 0.65625rem; color: var(--muted); }
/* Selection tick: only visible once chosen, so an unselected grid stays calm. */
.sd-lib-tick {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 22px; height: 22px; line-height: 20px; text-align: center;
  border-radius: 50%; font-size: 0.75rem; font-weight: 700;
  background: var(--accent); color: var(--surface-1, #fff);
  opacity: 0; transform: scale(0.8); transition: opacity 110ms linear, transform 110ms linear;
}
.sd-lib-item.selected .sd-lib-tick { opacity: 1; transform: scale(1); }

.sd-lib-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 16px; border-top: 1px solid var(--border);
}
.sd-lib-count { font-size: 0.75rem; color: var(--muted); }
.sd-lib-foot-actions { display: flex; gap: 8px; }
.sd-lib-cancel, .sd-lib-attach {
  padding: 9px 16px; cursor: pointer; font: inherit; font-size: 0.8125rem; font-weight: 600;
  border-radius: 8px; border: 1px solid var(--border);
}
.sd-lib-cancel { background: transparent; color: var(--muted); }
.sd-lib-cancel:hover { color: var(--text); }
.sd-lib-attach { background: var(--accent); color: var(--surface-1, #fff); border-color: var(--accent); }
.sd-lib-attach:disabled { opacity: 0.45; cursor: default; }

/* Mobile: the source menu and the picker both become bottom sheets, sized so
   the grid scrolls internally rather than the page (spec §15). */
@media (max-width: 767px) {
  .sd-attach-menu {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto;
    min-width: 0; padding: 6px 6px calc(6px + env(safe-area-inset-bottom, 0px));
    border-radius: 14px 14px 0 0;
  }
  .sd-attach-menu-item { min-height: 48px; font-size: 0.9375rem; }
  .sd-lib-backdrop { padding: 0; align-items: flex-end; }
  .sd-lib {
    width: 100%; max-width: 100%; max-height: 88vh;
    border-radius: 14px 14px 0 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .sd-lib-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; }
  .sd-lib { height: 88vh; }
  /* >=16px so focusing search does not zoom the page on iOS. */
  .sd-lib-search { font-size: 1rem; }
  .sd-lib-cancel, .sd-lib-attach { min-height: 44px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   GENERATION VIEWS — PREMIUM PASS
   branch: cinderloom-premium-redesign
   ───────────────────────────────────────────────────────────────────────────
   The ten places real work happens are Image, Characters, Character Sheet,
   Scene, Storyboard, Shot Extractor, Upscale, Video, Avatar and Workflow.
   Nine of them share one shell — .control-panel on the left, .viewfinder-panel
   on the right — so the rhythm below is applied once to that shell rather than
   ten times to ten copies. Workflow is a node canvas with neither, and is not
   touched here.

   EVERYTHING IS SCOPED to those nine view ids. Marketing, pricing, settings,
   explore and the editor are deliberately out of range: this is a pass on the
   working surfaces, not a site-wide restyle.

   The skill classified this product as AI Photo & Avatar Generator, whose
   primary recommendation is AI-Native UI + Aurora (purple, aurora gradients).
   That is declined — it would erase the brushed-silver identity the app is
   built on. What is taken is the SECONDARY: Minimalism & Swiss — grid, clear
   type hierarchy, no unnecessary decoration, subtle 200-250ms hover. The
   premium here comes from rhythm and restraint, not from a new palette.
   ═══════════════════════════════════════════════════════════════════════════ */

.gen-view,
#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
#view-shot-extractor, #view-remaster, #view-video, #view-avatar {
  /* One spacing scale for the working views. The form rhythm was a flat
     16px between every field regardless of whether two controls belonged
     together or not, which is why the panels read as a list rather than as
     grouped settings. */
  --g1: 4px; --g2: 8px; --g3: 12px; --g4: 18px; --g5: 26px; --g6: 36px;
}

/* ───────────────────────── 1. FIELD RHYTHM ────────────────────────────────
   A label and its control are one unit; units are separated by more air than
   the label sits from its own input. That single relationship is what makes a
   form scan as groups instead of as evenly-spaced rows. */
.gen-scope .field,
#view-image .field, #view-charsheet .field, #view-storyboard .field,
#view-remaster .field, #view-video .field, #view-avatar .field {
  margin-bottom: var(--g5);
}
#view-image .field > label, #view-charsheet .field > label,
#view-storyboard .field > label, #view-remaster .field > label,
#view-video .field > label, #view-avatar .field > label {
  margin-bottom: var(--g2);
}

/* Labels were --text-faint, the same grade as helper copy, so a field's NAME
   and its explanatory note carried identical weight. The name is the thing you
   scan for; it gets the darker grade and tighter tracking. */
#view-image label, #view-characters label, #view-charsheet label,
#view-scene label, #view-storyboard label, #view-shot-extractor label,
#view-remaster label, #view-video label, #view-avatar label {
  color: var(--text-muted);
  font-size: 0.65625rem;
  letter-spacing: 0.11em;
  font-weight: 700;
}

/* ───────────────────────── 2. PANEL SURFACES ──────────────────────────────
   Swiss says decoration only where it earns its place. The panels keep their
   brushed-metal identity; what changes is internal rhythm — more generous,
   consistent padding so controls are not pressed against the edge. */
#view-image .panel, #view-characters .panel, #view-charsheet .panel,
#view-scene .panel, #view-storyboard .panel, #view-shot-extractor .panel,
#view-remaster .panel, #view-video .panel, #view-avatar .panel {
  padding: var(--g5);
}
#view-image .control-panel, #view-charsheet .control-panel,
#view-storyboard .control-panel, #view-shot-extractor .control-panel,
#view-remaster .control-panel, #view-video .control-panel,
#view-avatar .control-panel, #view-characters .control-panel,
#view-scene .control-panel {
  display: flex;
  flex-direction: column;
}
/* The last field in a panel should not add space the panel is already
   providing. */
#view-image .control-panel > .field:last-of-type,
#view-charsheet .control-panel > .field:last-of-type,
#view-storyboard .control-panel > .field:last-of-type,
#view-remaster .control-panel > .field:last-of-type,
#view-video .control-panel > .field:last-of-type,
#view-avatar .control-panel > .field:last-of-type { margin-bottom: var(--g3); }

/* ───────────────────────── 3. CONTROLS ────────────────────────────────────
   Inputs sat at 10px/12px padding with 13.5px text — tight for a surface this
   size, and the focus ring was the only state that ever changed. A resting
   hover makes the whole form feel responsive rather than inert. */
#view-image :is(select, textarea, input[type="text"]),
#view-characters :is(select, textarea, input[type="text"]),
#view-charsheet :is(select, textarea, input[type="text"]),
#view-scene :is(select, textarea, input[type="text"]),
#view-storyboard :is(select, textarea, input[type="text"]),
#view-shot-extractor :is(select, textarea, input[type="text"]),
#view-remaster :is(select, textarea, input[type="text"]),
#view-video :is(select, textarea, input[type="text"]),
#view-avatar :is(select, textarea, input[type="text"]) {
  padding: 11px 13px;
  transition: border-color 200ms ease, box-shadow 200ms ease, background-color 200ms ease;
}
#view-image :is(select, textarea, input[type="text"]):hover:not(:focus),
#view-charsheet :is(select, textarea, input[type="text"]):hover:not(:focus),
#view-storyboard :is(select, textarea, input[type="text"]):hover:not(:focus),
#view-remaster :is(select, textarea, input[type="text"]):hover:not(:focus),
#view-video :is(select, textarea, input[type="text"]):hover:not(:focus),
#view-avatar :is(select, textarea, input[type="text"]):hover:not(:focus) {
  border-color: var(--text-secondary-dark);
}

/* ───────────────────────── 4. VIEWFINDER ──────────────────────────────────
   The preview is the largest object on every one of these screens and, before
   a generation exists, it was a single faint sentence floating in a very big
   dark rectangle. It now reads as a waiting instrument: a bounded plate, a
   quieter glyph, and the instruction given room rather than left adrift. */
#view-image .viewfinder-empty, #view-characters .viewfinder-empty,
#view-charsheet .viewfinder-empty, #view-scene .viewfinder-empty,
#view-storyboard .viewfinder-empty, #view-shot-extractor .viewfinder-empty,
#view-remaster .viewfinder-empty, #view-video .viewfinder-empty,
#view-avatar .viewfinder-empty {
  padding: var(--g6) var(--g5);
  border: 1px dashed var(--vf-border);
  border-radius: var(--radius-lg);
  max-width: 320px;
  margin: 0 auto;
}
#view-image .viewfinder-empty-glyph, #view-characters .viewfinder-empty-glyph,
#view-charsheet .viewfinder-empty-glyph, #view-scene .viewfinder-empty-glyph,
#view-storyboard .viewfinder-empty-glyph, #view-shot-extractor .viewfinder-empty-glyph,
#view-remaster .viewfinder-empty-glyph, #view-video .viewfinder-empty-glyph,
#view-avatar .viewfinder-empty-glyph {
  font-size: 1.375rem;
  margin-bottom: var(--g3);
  opacity: 0.55;
}
#view-image .viewfinder-empty p, #view-characters .viewfinder-empty p,
#view-charsheet .viewfinder-empty p, #view-scene .viewfinder-empty p,
#view-storyboard .viewfinder-empty p, #view-shot-extractor .viewfinder-empty p,
#view-remaster .viewfinder-empty p, #view-video .viewfinder-empty p,
#view-avatar .viewfinder-empty p {
  font-size: 0.78125rem; line-height: 1.55; letter-spacing: 0.01em;
}

/* The viewfinder header is a status strip; it should read as instrumentation,
   not as a page heading. */
#view-image .viewfinder-head, #view-characters .viewfinder-head,
#view-charsheet .viewfinder-head, #view-scene .viewfinder-head,
#view-storyboard .viewfinder-head, #view-shot-extractor .viewfinder-head,
#view-remaster .viewfinder-head, #view-video .viewfinder-head,
#view-avatar .viewfinder-head {
  padding: var(--g3) var(--g4);
}

/* ───────────────────────── 5. VIEW HEADERS ────────────────────────────────
   Title and its one-line description were stacked at default spacing, so the
   description read as a second heading rather than as support. */
#view-image > h1, #view-characters > h1, #view-charsheet > h1,
#view-scene > h1, #view-storyboard > h1, #view-shot-extractor > h1,
#view-remaster > h1, #view-video > h1, #view-avatar > h1 {
  margin-bottom: var(--g1);
  letter-spacing: -0.015em;
  text-wrap: balance;
}
#view-image > h1 + p, #view-characters > h1 + p, #view-charsheet > h1 + p,
#view-scene > h1 + p, #view-storyboard > h1 + p, #view-shot-extractor > h1 + p,
#view-remaster > h1 + p, #view-video > h1 + p, #view-avatar > h1 + p {
  color: var(--text-faint);
  font-size: 0.8125rem;
  max-width: 60ch;
  margin-bottom: var(--g5);
}

@media (prefers-reduced-motion: reduce) {
  #view-image *, #view-characters *, #view-charsheet *, #view-scene *,
  #view-storyboard *, #view-shot-extractor *, #view-remaster *,
  #view-video *, #view-avatar * { transition: none !important; }
}

/* The credit cost is the number a person decides on, and it was set at 800
   weight inside the button at 0.85 opacity — quieter than the word Generate
   next to it. It reads as a price now: full strength, its own chip, tabular
   figures so it does not jitter as the batch size changes. */
#view-image .btn-credit, #view-characters .btn-credit, #view-charsheet .btn-credit,
#view-scene .btn-credit, #view-storyboard .btn-credit, #view-shot-extractor .btn-credit,
#view-remaster .btn-credit, #view-video .btn-credit, #view-avatar .btn-credit {
  opacity: 1;
  font-variant-numeric: tabular-nums;
  font-size: 0.82em;
  letter-spacing: 0.04em;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.16);
}

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 3 — ANCHORED ON WHAT THE VIEWS ACTUALLY SHARE
   ───────────────────────────────────────────────────────────────────────────
   Correcting an assumption in the previous pass. The nine working views do NOT
   share one shell. Measured across index.html:

     console-grid        image, charsheet                        (2)
     video-console-grid  video, avatar                           (2)
     no named grid       characters, scene, storyboard,
                         shot-extractor, remaster                (5)
     .control-panel      absent entirely from characters, scene
     .field              absent entirely from scene

   The previous pass hung its rhythm on .field and .control-panel, so scene got
   nothing at all. What IS universal is the label: .section-label appears 119
   times, in all nine views, scene included. Anchoring the rhythm on the label
   rather than the wrapper reaches every view with one rule instead of one rule
   per shell that silently misses the views built differently.
   ═══════════════════════════════════════════════════════════════════════════ */

/* :is() with ids carries id specificity, so one selector does the work of nine
   prefixed ones and still beats the base .section-label rule. */
@supports selector(:is(a)) {

/* ─────────────────── 1. GROUPS, ANCHORED ON THE LABEL ─────────────────────
   A label starts a group. Giving the separation to the label rather than to a
   wrapper means it lands in scene (.scene-section), in remaster and avatar
   (.section-label-row) and in image and video (.field) all at once. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(.section-label-row, .field > .section-label, .scene-section > .section-label) {
  margin-top: var(--g6);
}
/* A label inside a row is positioned by the row; it must not add its own. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  .section-label-row > .section-label {
  margin-top: 0;
}
/* The first group in any container sits against padding that container already
   provides. This resets the unit only where it is genuinely first in its OWN
   parent — an earlier version reset every descendant of a panel's first child,
   which silently cancelled all ten groups in storyboard's control column. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(.panel, .control-panel, .storyboard-control-col)
  > :is(.field:first-child > .section-label, .section-label-row:first-child) {
  margin-top: 0;
}
/* scene has no .panel at all; its groups ARE .scene-section, so the label is
   always its section's first child. Only the first SECTION resets, otherwise
   the rule cancels every group in the view. */
#view-scene .scene-section:first-child > .section-label { margin-top: 0; }

/* The pinned primary action was removed after review. Sticky pins to its own
   panel, so the button held position while the panel was in view and then
   released and scrolled away once the panel bottom passed. That half-pinned,
   half-scrolling motion read as inconsistent, which is worse than the problem
   it solved. The action sits in normal flow at the end of the form.

   The underlying issue is still real: Generate, and the credit price on it,
   sit below the fold on a laptop. If it is worth solving later, the fix is a
   genuinely fixed action bar that never moves, not a sticky one that
   sometimes does. */

}

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 4 — MATERIAL, AGAINST A REAL REFERENCE
   ───────────────────────────────────────────────────────────────────────────
   Passes 1-3 adjusted rhythm and were correctly judged invisible. Spacing was
   never the problem.

   Measured cause: .panel is background var(--surface-2) and every select and
   textarea is ALSO var(--surface-2). Identical fill. The only thing separating
   a control from the surface holding it is a 1px PURE BLACK outline, and the
   panel carries the same pure black outline. One flat value plus hard uniform
   outlines is what unstyled form markup looks like.

   Direction sampled from app.plumar.ai rather than guessed. Real values off
   that page:

       card border    rgba(255,255,255,0.55)      light, not black
       card radius    28px
       card shadow    0 40px 90px rgba(15,20,35,.28), 0 8px 24px rgba(...,.12)
       input bg       rgba(255,255,255,0.5)       translucent, no shadow
       input radius   14px
       input weight   300
       heading        600 / 32px / -0.96px tracking
       button         999px pill, no border, 0 6px 20px rgba(15,20,35,.18)

   The transferable idea: edges are LIGHT and catch the light, shadows are
   large and diffuse, radii are generous, surfaces are translucent, and type
   is light with tight tracking. ShotBorn keeps its own palette, its black
   accent and its dark viewfinder; what it adopts is that material language.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* Light-catching edges instead of drawn black outlines. On silver these read
     as a machined bevel: lit on top, faintly shadowed underneath. */
  --edge-lit:    rgba(255, 255, 255, 0.75);
  --edge-quiet:  rgba(10, 20, 35, 0.10);
  /* Large and diffuse. The old shadow was 0 10px 28px; this is the depth that
     makes a surface look like it is floating rather than pasted on. */
  --lift-lg: 0 36px 80px -28px rgba(12, 20, 35, 0.42),
             0 10px 26px -12px rgba(12, 20, 35, 0.20);
  --lift-sm: 0 6px 20px -6px rgba(12, 20, 35, 0.22);
  --r-field: 14px;
  --r-panel: 22px;
}

/* ─────────────────────── 1. PANELS FLOAT, NOT OUTLINED ────────────────────
   Black outline removed. The plate is defined by a bright translucent top
   edge, a barely-there outer hairline, and a deep soft shadow underneath. */
.panel {
  background:
    linear-gradient(168deg, rgba(255,255,255,0.92), rgba(255,255,255,0.35) 46%),
    var(--silver-polished);
  border: 1px solid var(--edge-lit);
  border-radius: var(--r-panel);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.95),
    inset 0 0 0 1px rgba(255,255,255,0.45),
    0 0 0 1px var(--edge-quiet),
    var(--lift-lg);
  padding: 26px;
}

/* ───────────────────── 2. CONTROLS: TRANSLUCENT, NOT BOXED ────────────────
   The single highest-leverage change. A field is now a translucent pane on
   the plate, edged in light. No black outline, larger radius, lighter text. */
select, textarea,
input[type="text"], input[type="password"], input[type="email"],
input[type="number"], input[type="search"] {
  background: rgba(255, 255, 255, 0.62);
  border: 1px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 0 0 1px rgba(10, 20, 35, 0.07), 0 1px 2px rgba(12,20,35,0.05);
  border-radius: var(--r-field);
  padding: 12px 15px;
  font-weight: 400;
  transition: background-color 180ms ease, box-shadow 180ms ease;
}
select:hover:not(:focus), textarea:hover:not(:focus),
input[type="text"]:hover:not(:focus) {
  background: rgba(255, 255, 255, 0.85);
  box-shadow: 0 0 0 1px rgba(10, 20, 35, 0.12), var(--lift-sm);
}
select:focus, textarea:focus, input:focus {
  background: #FFFFFF;
  border-color: #FFFFFF;
  box-shadow: 0 0 0 1px rgba(10,20,35,0.30), 0 8px 24px -8px rgba(12,20,35,0.28);
  outline: none;
}

/* ────────────────────────── 3. THE PILL ACTION ────────────────────────────
   Black is kept: it is the brand accent and the loudest mark available on a
   silver chassis. What changes is the shape and the lift. */
.primary-btn {
  border-radius: 999px;
  padding: 15px 24px;
  letter-spacing: 0.01em;
  box-shadow: 0 8px 24px -6px rgba(10, 10, 10, 0.45), 0 2px 6px rgba(10,10,10,0.20);
  transition: transform 160ms ease, box-shadow 160ms ease, filter 160ms ease;
}
.primary-btn:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 14px 34px -8px rgba(10, 10, 10, 0.52), 0 3px 8px rgba(10,10,10,0.24);
}
.primary-btn:active:not(:disabled) { transform: translateY(0); }

/* ─────────────────── 4. TYPE: TIGHTER, LIGHTER, BRANDED ───────────────────
   Headings take the reference's negative tracking. Labels move to Archivo,
   the brand display face already loaded; .section-label previously asked for
   --font-mono, which resolves to Inter, so the app's most repeated label was
   set in the same face as body copy, just smaller. */
.view > .panel-head h1, .panel-head h1 {
  letter-spacing: -0.03em;
  font-weight: 700;
}
.section-label, .field > label {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.65625rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: rgba(17, 17, 17, 0.58);
}

/* ───────────────────────────── 5. DENSITY ─────────────────────────────────
   Pass 3 opened groups to 36px. Pulled back: dense and precise reads as an
   instrument, airy reads as a marketing page. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) {
  --g5: 20px;
  --g6: 24px;
}

/* ─────────────────── 6. THE VIEWFINDER BECOMES A RECESS ───────────────────
   The dark aperture is the signature. It met the chassis with a flat black
   line; now it has a lit outer edge and depth, so it reads as cut into the
   machine rather than pasted over it. */
.viewfinder-panel {
  border: 1px solid rgba(255, 255, 255, 0.55);
  border-radius: var(--r-panel);
  box-shadow:
    inset 0 2px 6px rgba(0, 0, 0, 0.75),
    inset 0 0 0 1px rgba(0, 0, 0, 0.85),
    var(--lift-lg);
}

@media (prefers-reduced-motion: reduce) {
  select, textarea, input, .primary-btn { transition: none; }
  .primary-btn:hover:not(:disabled) { transform: none; }
}

/* ─────────────── 7. SECONDARY CONTROLS JOIN THE SAME LANGUAGE ─────────────
   Converting panels and fields but leaving these behind made the pass read as
   half-finished: "Choose from library" in particular stayed a grey slab with a
   hard black outline sitting inside a soft translucent panel. Audited from the
   rendered page, these were the nine control types still drawing pure black
   1px borders. They get the light edge, the pill radius and the soft lift. */
.mobile-back-btn,
.elements-trigger-btn,
.enhance-btn,
.lib-pick-btn,
.moodboard-selector-btn,
.segmented {
  background: rgba(255, 255, 255, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.9);
  box-shadow: 0 0 0 1px rgba(10, 20, 35, 0.08), 0 2px 6px -2px rgba(12,20,35,0.14);
  border-radius: 999px;
  transition: background-color 180ms ease, box-shadow 180ms ease;
}
.mobile-back-btn:hover,
.elements-trigger-btn:hover,
.enhance-btn:hover,
.lib-pick-btn:hover,
.moodboard-selector-btn:hover {
  background: #FFFFFF;
  box-shadow: 0 0 0 1px rgba(10, 20, 35, 0.14), var(--lift-sm);
}
/* The moodboard selector is a full-width field, not a chip, so it keeps the
   field radius rather than becoming a very long pill. */
.moodboard-selector-btn { border-radius: var(--r-field); }
/* The segmented control is a track holding its own buttons; it should read as
   a recess, not as a raised chip. */
.segmented {
  background: rgba(10, 20, 35, 0.06);
  border-color: rgba(255,255,255,0.7);
  box-shadow: inset 0 1px 2px rgba(12,20,35,0.10);
}

/* Dashed drop targets keep the dashed line, which is meaningful, but lose the
   full-strength black. */
.elements-dropzone,
.gallery-empty,
.viewfinder-empty {
  border-color: rgba(10, 20, 35, 0.22);
  border-radius: var(--r-field);
}

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 5 — INVERT THE SHELL
   ───────────────────────────────────────────────────────────────────────────
   From the app.plumar.ai screenshot: the premium there is not panel styling.
   It is that the CHROME IS DARK AND RECEDES while the work surface is a large
   soft canvas with very big radii and controls floating over it. Nothing is
   outlined. One thing is the hero.

   ShotBorn had three competing zones: a light rail, a light dotted silver
   ground, and a dark viewfinder, all sharing one --surface-gradient. Nothing
   receded, so nothing read as the subject.

   Not copied literally. Plumar's canvas is light because it edits nodes;
   ShotBorn's canvas is the VIEWFINDER, and generated imagery has to be
   judged against a dark neutral ground. So the inversion here is: dark shell,
   viewfinder merging into it so the image floats nearly frameless, and the
   silver control panel left as the single bright instrument.

   No new palette. Every dark value below is an existing --vf-* token already
   shipping in the viewfinder, so the colour identity is extended rather than
   replaced.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* The dot grid has to invert with the ground it sits on. */
  --black-dots: rgba(255, 255, 255, 0.07);
  --shell-ink:       #EDEDF0;
  --shell-ink-muted: #9A9AA4;
  --shell-ink-faint: #6A6A74;
}

/* ───────────────────────────── 1. THE SHELL ───────────────────────────────
   Built from --vf-bg and --vf-surface, the viewfinder's own tokens. */
body {
  background:
    radial-gradient(120% 80% at 78% -10%, rgba(90, 100, 120, 0.16), transparent 60%),
    linear-gradient(168deg, #141417 0%, #0C0C0F 52%, #08080A 100%) !important;
  background-attachment: fixed !important;
  color: var(--shell-ink);
}

/* main paints its own brushed-silver gradient on top of body, so the shell is
   not dark until this is inverted too. The dot grid and the fine brushed
   striping are kept, because they are the surface texture that makes the
   chassis feel like metal rather than flat paint. Only the base layer flips. */
main {
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(rgba(255,255,255,0.030) 0px, rgba(255,255,255,0.030) 1px,
                              transparent 1px, transparent 3px),
    repeating-linear-gradient(rgba(255,255,255,0.022) 0px, transparent 40px,
                              rgba(255,255,255,0.030) 80px, transparent 120px),
    linear-gradient(168deg, #141417 0%, #0C0C0F 52%, #08080A 100%) !important;
  background-size: 22px 22px, auto, auto, auto !important;
  background-attachment: fixed, fixed, fixed, fixed !important;
}

/* ────────────────────────── 2. THE RAIL RECEDES ───────────────────────────
   No fill of its own, no black divider. It sits on the shell and is separated
   by a single hairline of light, the way the reference does it. */
.rail {
  background: transparent !important;
  border-right: 0 !important;
  box-shadow: none !important;
}
.rail-section, .nav-group-label { color: var(--shell-ink-faint) !important; }
.rail-btn, .rail-btn span, .rail-account { color: var(--shell-ink-muted); }
.rail-btn:hover { background: rgba(255,255,255,0.06); color: var(--shell-ink); }
.rail-btn.active, .rail-btn[aria-current="page"] {
  background: rgba(255, 255, 255, 0.10);
  color: #FFFFFF;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.14);
}

/* ─────────────────── 3. TEXT SITTING DIRECTLY ON THE SHELL ────────────────
   These were black on silver and would be invisible on the dark ground. */
.panel-head h1 { color: var(--shell-ink) !important; letter-spacing: -0.03em; }
.panel-head .panel-sub { color: var(--shell-ink-muted) !important; }
main > h1, main > h2, .view > h1, .view > h2 { color: var(--shell-ink); }

/* ──────────────── 4. THE VIEWFINDER MERGES INTO THE SHELL ─────────────────
   Its frame was doing the work of separating it from a light page. On a dark
   shell that frame is what stops the image from reading as the subject, so it
   goes almost entirely: a whisper of an edge and no lift. */
/* SUPERSEDED. This was written for the dark shell, where 34% black read as
   near-black. The generation pages are silver again, and the viewfinder only
   ever appears on those, so the same value is now a mid grey - which is why
   the empty-state glyph and copy measured 1.97:1 against it. Worse, all four
   declarations were !important and blocked every later correction.

   The aperture is opaque dark, which is the entire point of it: a neutral
   ground to judge generated media against. */
.viewfinder-panel {
  background: linear-gradient(180deg, var(--vf-surface-2), var(--vf-bg));
  background-color: var(--vf-bg);
  border: 1px solid rgba(10, 10, 10, 0.55);
  border-radius: 26px;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.65),
    inset 0 1px 3px rgba(0, 0, 0, 0.6),
    0 18px 44px -20px rgba(10, 14, 20, 0.5);
}

/* ───────────────── 5. THE CONTROL PANEL IS THE ONE BRIGHT OBJECT ──────────
   Unchanged in material from pass 4, but on a dark shell it now reads as an
   instrument set into the machine rather than one grey rectangle among
   several. Radius grows to match the reference's generous corners. */
.panel {
  border-radius: 28px;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.95),
    inset 0 0 0 1px rgba(255,255,255,0.45),
    0 30px 70px -24px rgba(0, 0, 0, 0.75),
    0 8px 20px -10px rgba(0, 0, 0, 0.45);
}

/* Cards and surfaces that were light-on-light and are now light-on-dark keep
   their own ink; only what sits on the bare shell needed flipping. */

/* Contrast repair for the inverted shell. Measured against the rail ground:
   the hamburger toggle came out at 1.01:1 and the BETA badge at 1.06:1, both
   effectively invisible because they were black-on-silver elements that the
   inversion left behind. */
.rail-toggle {
  color: var(--shell-ink-muted) !important;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.10);
}
.rail-toggle:hover { color: #FFFFFF !important; background: rgba(255,255,255,0.12); }
.rail-beta-badge {
  color: rgba(255,255,255,0.82) !important;
  background: rgba(255,255,255,0.10) !important;
  border: 1px solid rgba(255,255,255,0.14) !important;
}
/* The collapse chip was a brushed-silver gradient keyed to the old light rail. */
.rail-collapse-btn {
  background: rgba(255,255,255,0.07) !important;
  border: 1px solid rgba(255,255,255,0.12) !important;
  color: var(--shell-ink-muted) !important;
}

/* The previous repair guessed at class names (.rail-section, .nav-group-label)
   and missed. Audited from the rendered page instead: these are the real ones,
   measured at 1.01:1 and 1.04:1 against the rail ground. */
.rail-group-label {
  color: rgba(255, 255, 255, 0.42) !important;
  letter-spacing: 0.16em;
}
.rail-btn-icon { color: var(--vf-text-muted) !important; }
.rail-btn.active .rail-btn-icon,
.rail-btn[aria-current="page"] .rail-btn-icon { color: var(--vf-text) !important; }

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 6 — INSTRUMENT LANGUAGE
   ───────────────────────────────────────────────────────────────────────────
   From the Plumar node screenshots. Three things carry that interface, and
   none of them are spacing or shadow:

     1. A real monospace, uppercase, for every label, chip and status readout
        (DP, COOKE S4/I 50MM, IDLE, AWAITING UPSTREAM).
     2. A coloured port dot on every input row.
     3. One hue per category, used on the eyebrow, the dot and the edge, and
        nowhere else.

   ShotBorn had no monospace loaded at all: --font-mono resolved to Inter, so
   .section-label, the app's most repeated object, was body copy at a smaller
   size pretending to be instrumentation. IBM Plex Mono is now loaded, which
   also pairs with the IBM Plex Sans already in the stack.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  --font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  /* The per-view hue is gone. It marked one panel edge per view - teal on
     Image, orange on Video, pink on Avatar - and read as a coloured lining
     rather than as information. The label rings were already unified to
     black, so with the panel edge removed the chrome spends no hue at all;
     the node categories on the workflow canvas are the only place colour
     still identifies something. */
}

/* ─────────────────────── 1. LABELS BECOME READOUTS ────────────────────────
   Real mono, tracked, with a port dot in the view's hue. */
.section-label,
.field > label {
  font-family: var(--font-mono) !important;
  font-weight: 500;
  font-size: 0.625rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: rgba(17, 17, 17, 0.55);
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(.section-label, .field > label)::before {
  content: "";
  width: 7px; height: 7px;
  border-radius: 999px;
  flex: none;
  border: 1.5px solid var(--accent);
  /* Hollow ring, like the reference's unconnected ports. */
  background: transparent;
}

/* ───────────────────── 2. THE PANEL CARRIES ITS HUE ───────────────────────
   A single tinted edge, so you can tell which instrument is open at a glance
   without the colour ever touching the content. */
/* The output panel is also a .panel, and on Video, Avatar and Shot Extractor
   it comes FIRST in the DOM - so :first-of-type put the instrument hue on the
   viewfinder instead of the controls. Orange, pink and rose read as a red
   lining around the output.

   :not(.viewfinder-panel) alone does not fix it: :first-of-type still means
   the first div among siblings, so excluding it matches nothing rather than
   falling through to the control panel. The selector has to name the control
   surface. .panel:first-of-type stays for Characters, whose control surface is
   .char-style-panel, not .control-panel. Scene has no .panel at all and so
   carries no tint, which matches its documented shell. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(.control-panel, .panel:first-of-type):not(.viewfinder-panel):not(.flyout-panel) {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.95),
    inset 0 0 0 1px rgba(255,255,255,0.45),
    0 30px 70px -24px rgba(0,0,0,0.75),
    0 8px 20px -10px rgba(0,0,0,0.45);
}

/* ───────────────────── 3. STATUS READS AS INSTRUMENT ──────────────────────
   Uppercase mono for state, the way the reference prints IDLE and AWAITING
   UPSTREAM rather than sentence-case chat copy. */
.viewfinder-head, .status-line, .generate-helper, .btn-credit, .vf-status {
  font-family: var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-size: 0.625rem;
}
.btn-credit { font-size: 0.78em; letter-spacing: 0.06em; }

/* ─────────────────── 4. CONTRAST REPAIR, ROUND TWO ────────────────────────
   Audited again from the rendered page after the inversion. The account block
   at the foot of the rail was still black-on-silver: 1.01:1 and 1.04:1. */
.account-name  { color: var(--shell-ink) !important; }
.account-sub   { color: var(--shell-ink-faint) !important; }
.rail-account-chevron { color: var(--shell-ink-faint) !important; }
.account-avatar {
  color: #FFFFFF !important;
  background: rgba(255,255,255,0.14) !important;
  border: 1px solid rgba(255,255,255,0.18) !important;
}
.rail-account:hover { background: rgba(255,255,255,0.06); }

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 7 — DELICACY
   ───────────────────────────────────────────────────────────────────────────
   The reference reads fine and light. Measured, it gets there by being
   consistently LARGER AND LIGHTER than ShotBorn, not smaller and denser:

       heading   600 weight, -0.03em          ShotBorn: Archivo 800
       input     15px / weight 300            ShotBorn: 13.5px / 400
       button    grey pill, modest shadow     ShotBorn: black slab, hard lift
       edges     barely-there hairlines       ShotBorn: 3px accent bar

   Every rule below reduces weight, thins a stroke or softens a shadow. None
   of it changes colour, layout or behaviour. Contrast is re-audited after,
   because lightening type is exactly how legibility gets lost.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─────────────────── 1. TYPE: BIGGER, LIGHTER, TIGHTER ────────────────────
   Archivo 800 is a poster weight. At 600 the title still leads the page and
   stops shouting; the extra size and negative tracking carry the authority
   the weight was doing badly. */
.panel-head h1 {
  font-weight: 600 !important;
  font-size: clamp(26px, 2.4vw, 32px);
  letter-spacing: -0.03em;
}
.panel-head .panel-sub {
  font-weight: 300;
  font-size: 0.90625rem;
  line-height: 1.55;
}

/* Controls follow the reference: a larger face at a lighter weight is easier
   to read than a small one at 400, and it is what makes a dense form feel
   unhurried. */
select, textarea,
input[type="text"], input[type="password"], input[type="email"],
input[type="number"], input[type="search"] {
  font-size: 0.90625rem;
  font-weight: 350;
  padding: 13px 16px;
  border: 1px solid rgba(255, 255, 255, 0.7);
  /* Softer than before: the outer hairline drops from 0.07 to 0.05 alpha and
     the drop shadow goes entirely. A field should sit, not hover. */
  box-shadow: 0 0 0 1px rgba(10, 20, 35, 0.05);
}
select:hover:not(:focus), textarea:hover:not(:focus),
input[type="text"]:hover:not(:focus) {
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 0 0 1px rgba(10, 20, 35, 0.09);
}
select:focus, textarea:focus, input:focus {
  box-shadow: 0 0 0 1px rgba(10, 20, 35, 0.22), 0 4px 14px -6px rgba(12, 20, 35, 0.18);
}

/* Labels: one step lighter and one step smaller, still legible because mono
   at wide tracking holds up where a proportional face would not. */
.section-label, .field > label {
  font-weight: 450;
  font-size: 0.59375rem;
  letter-spacing: 0.17em;
  gap: 7px;
}
/* A finer port ring. 1.5px at 7px across read as a bullet; 1.25px at 6px
   reads as a port. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(.section-label, .field > label)::before {
  width: 6px; height: 6px;
  border-width: 1.25px;
}

/* ────────────────────── 2. THE PANEL STOPS PRESSING ───────────────────────
   The old lift was 0 30px 70px at 0.75 alpha, which is a stage light. Halved,
   and the accent bar thinned from 3px to 2px so it marks the panel instead of
   bracketing it. */
.panel {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    inset 0 0 0 1px rgba(255, 255, 255, 0.4),
    0 24px 60px -30px rgba(0, 0, 0, 0.55),
    0 4px 14px -8px rgba(0, 0, 0, 0.3);
}
/* Second copy of the instrument-hue edge, with the same :first-of-type bug
   as the one above: on Video, Avatar and Shot Extractor the viewfinder is
   the first .panel, so the hue landed on the output. Same fix - name the
   control surface rather than pick by position. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(.control-panel, .panel:first-of-type):not(.viewfinder-panel):not(.flyout-panel) {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    inset 0 0 0 1px rgba(255, 255, 255, 0.4),
    0 24px 60px -30px rgba(0, 0, 0, 0.55),
    0 4px 14px -8px rgba(0, 0, 0, 0.3);
}

/* ───────────────────── 3. THE ACTION STOPS SHOUTING ───────────────────────
   The reference's own Generate is a modest grey pill, not a black slab with a
   hard drop. Black is kept because it is the brand accent, but softened to
   near-black, given a lighter label and a shadow that suggests rather than
   announces. */
.primary-btn {
  background: #17171A;
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  padding: 14px 24px;
  box-shadow: 0 6px 18px -8px rgba(10, 10, 10, 0.4);
}
.primary-btn:hover:not(:disabled) {
  background: #202024;
  box-shadow: 0 10px 26px -10px rgba(10, 10, 10, 0.45);
}
.btn-credit { background: rgba(255, 255, 255, 0.13); font-weight: 450; }

/* ──────────────────── 4. SECONDARY CONTROLS GO QUIETER ────────────────────
   These were pills with their own outer hairline AND a drop shadow, which is
   two announcements for a control that should barely register until wanted. */
.mobile-back-btn, .elements-trigger-btn, .enhance-btn,
.lib-pick-btn, .moodboard-selector-btn {
  font-weight: 450;
  box-shadow: 0 0 0 1px rgba(10, 20, 35, 0.06);
  background: rgba(255, 255, 255, 0.66);
}

/* ─────────────────────── 5. THE RAIL WHISPERS TOO ─────────────────────────
   Nav at 400 rather than the inherited semibold, so the active item is the
   only thing with weight in the column. */
.rail-btn { font-weight: 400; letter-spacing: 0.005em; }
.rail-btn.active, .rail-btn[aria-current="page"] { font-weight: 500; }
.rail-group-label { font-family: var(--font-mono); font-size: 0.5625rem; font-weight: 450; }

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 7b — INK REPAIR AFTER THE INVERSION
   ───────────────────────────────────────────────────────────────────────────
   Audited from the rendered page. Two opposite failures, both mine:

   A. Dark text stranded on the dark shell. Scene is the only view with no
      .panel, so its groups sit directly on the ground. .scene-group-title was
      rgb(10,10,10) on near-black at 1.04:1.

   B. Light text stranded on light cards. `body { color: --shell-ink }` is
      needed for text on the shell, but every element without an explicit
      colour inherited it, including text inside the light cards.
      .style-locked-name rendered #EDEDF0 on a #EEF1F3 card at 1.03:1.

   A is fixed structurally rather than by recolouring: scene gets the same
   panel surface as the other eight views, which is what it should have had.
   B is fixed by resetting ink on light surfaces, so inheritance stops leaking
   shell colour into them.
   ═══════════════════════════════════════════════════════════════════════════ */

/* A. Scene's groups become real surfaces. */
#view-scene .scene-section {
  background:
    linear-gradient(168deg, rgba(255,255,255,0.92), rgba(255,255,255,0.35) 46%),
    var(--silver-polished);
  border: 1px solid var(--edge-lit);
  border-radius: var(--r-panel);
  padding: 22px 24px;
  margin-bottom: var(--g6);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.9),
    inset 0 0 0 1px rgba(255,255,255,0.4),
    0 24px 60px -30px rgba(0,0,0,0.55),
    0 4px 14px -8px rgba(0,0,0,0.3);
}
#view-scene .scene-section > .section-label:first-child { margin-top: 0; }

/* B. Any light surface carries dark ink, regardless of what body sets.
   Enumerated from the rendered DOM across all nine views (every element whose
   computed background is opaque and above 0.55 luminance) rather than guessed
   at, because a partial list is what left light text on light cards. */
.panel, .scene-section, .scene-elements-panel, .scene-history-reuse,
.scene-upload-tile, .entity-card, .style-card, .style-locked-card,
.live-prompt-panel, .remaster-model-info, .video-console-bottom,
.elements-trigger-btn, .enhance-btn, .lib-pick-btn, .mobile-back-btn,
.moodboard-selector-btn, .icon-btn, .text-btn, .seg-btn, .pill-toggle,
.pill-swatch, .type-switch, .value-stepper, .text-input, .thumb,
.visual-card-thumb, .visual-selector-arrow {
  color: var(--text);
}

/* Elements that genuinely sit on the bare shell and still carried chassis ink. */
#view-scene .scene-outputs-count,
.panel-head .panel-sub,
.view > .section-hint {
  color: var(--shell-ink-muted) !important;
}

/* Enumerated from the rendered pages across all nine views rather than named
   by hand, after guessing class names wrong twice. These are every element
   left with dark ink on the bare shell, plus the one with light ink on a
   light surface. */
/* These classes appear BOTH on the bare shell and inside light panels
   depending on the view, so pinning them to one colour breaks whichever case
   it is not: forcing them light took .section-hint from 1.90 on the shell to
   2.46 inside a panel. They inherit instead, muted relative to whatever ink
   their surface provides, which is correct on both grounds at once. */
.section-hint,
.scene-group-title,
.scene-subgroup-title,
.media-type-label,
.panel-sub,
.gallery-empty,
.seg-btn {
  color: color-mix(in srgb, currentColor 74%, transparent) !important;
}
.scene-group-title { color: currentColor !important; }
/* The selected segment gets a defined chip so its ink is never guesswork. */
.seg-btn.active, .seg-btn[aria-selected="true"] {
  color: #17171A !important;
  background: rgba(255,255,255,0.92) !important;
}
/* Empty-state copy inside the viewfinder was --vf-text-faint on near-black,
   below 3:1 before any of this work. Lifted to the viewfinder's muted grade. */
.viewfinder-empty p, .viewfinder-empty-glyph { color: var(--vf-text-muted) !important; }
.viewfinder-empty-glyph { opacity: 0.8; }
/* White label on a light entity card needs ink — but the same button on a
   media-only card sits on the picture's black, where ink was 1.1:1. */
.entity-card:not(.entity-card-media-only) .image-tag-add-btn { color: var(--text) !important; }
.entity-card-media-only .image-tag-add-btn { color: #fff !important; }

/* Final four, identified by element rather than by class guess:

   - <h3>Scene History</h3> inside .section-label-row carried rgb(17,17,17)
     on the shell at 1.02:1. It is an unclassed heading, so it inherits now
     and is correct on both a panel and the bare shell.
   - The viewfinder empty state ran --vf-text-muted at 2.69:1. Lifted to the
     label grade; this was below 3:1 before any of this work too.
   - .section-hint inside .generate-helper inherited chassis ink onto shell. */
.section-label-row h3, .section-label-row h2, .section-label-row h4 {
  color: currentColor;
}
/* Scoped to the dark panel: scene's .viewfinder-empty is NOT inside
   .viewfinder-panel, it sits in a .scene-section, which is light. Unscoped,
   this rule put light text on a light card at 1.21:1. Scene's copy inherits
   its section's dark ink instead, which is right on that ground. */
.viewfinder-panel .viewfinder-empty p { color: var(--vf-text-label) !important; }
.viewfinder-panel .viewfinder-empty-glyph { color: var(--vf-text-muted) !important; opacity: 1; }
.generate-helper { color: var(--shell-ink-faint); }
.panel .generate-helper, .control-panel .generate-helper { color: var(--text-muted); }
/* Scene's empty state lives on a light .scene-section, so it needs the chassis
   muted grade rather than the viewfinder's. Was 2.69:1. */
.scene-section .viewfinder-empty p,
.scene-section .viewfinder-empty-glyph { color: var(--text-muted) !important; }
/* Home's enter button: white label on what became a light chip. */
.home-enter-btn { color: #FFFFFF !important; background: #17171A !important; border: none !important; }

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 8 — VALUES FROM SKIPER-UI
   ───────────────────────────────────────────────────────────────────────────
   Sampled from skiper-ui.com. Design metrics only: colours, radii and type
   measurements. None of their component code is used here.

       ground        rgb(8,8,8)
       ink           rgb(237,237,237)
       surface       rgb(18,18,18)      10 points off the ground
       hairline      rgb(22,22,22)       4 points off the surface
       radius        16px, same on the nav pill and the action
       action        15px / 500, no shadow at all
       display       96px / line-height 1.00

   Two things worth taking. Its value steps are nearly invisible, which is
   where the cleanness comes from: separation by the smallest step that still
   reads, not the most comfortable one. And display type sits at line-height
   1.00, which ShotBorn was not doing anywhere.

   Its ink is within a point of the --shell-ink already chosen here, so that
   stays as it is.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─────────────── 1. THE SHELL TAKES THE TIGHTER VALUE STEPS ───────────────
   Ground drops to 8,8,8 and the rail becomes a real surface at 18,18,18 with
   a 22,22,22 hairline, rather than a wash of white at 2% opacity. */
body {
  background:
    radial-gradient(120% 80% at 78% -10%, rgba(90,100,120,0.10), transparent 62%),
    linear-gradient(168deg, #0E0E10 0%, #0A0A0B 55%, #080808 100%) !important;
}
main {
  background:
    radial-gradient(var(--black-dots) 1px, transparent 1px),
    repeating-linear-gradient(rgba(255,255,255,0.022) 0px, rgba(255,255,255,0.022) 1px,
                              transparent 1px, transparent 3px),
    linear-gradient(168deg, #0E0E10 0%, #0A0A0B 55%, #080808 100%) !important;
  background-size: 22px 22px, auto, auto !important;
}
.rail {
  background: transparent !important;
  border-right: 0 !important;
}

/* ─────────────────────── 2. DISPLAY TYPE CLOSES UP ────────────────────────
   line-height 1.00. At 26-32px this is the single most visible change in the
   pass: the title stops floating in its own leading. */
.panel-head h1 {
  line-height: 1.0;
  margin-bottom: 10px;
}

/* ──────────────────────── 3. ONE RADIUS, NOT THREE ────────────────────────
   Skiper uses 16px on both its chrome pill and its action. ShotBorn had 14
   for fields and 999 for every secondary control, so nothing agreed. Fields
   and secondary controls unify at 16; panels stay larger because they are a
   different order of object. */
:root { --r-field: 16px; }
.mobile-back-btn, .elements-trigger-btn, .enhance-btn,
.lib-pick-btn, .moodboard-selector-btn, .seg-btn, .type-switch {
  border-radius: 16px;
}

/* ──────────────────────── 4. THE ACTION GOES FLAT ─────────────────────────
   Their action carries no shadow at all. A solid fill at a confident radius
   does not need lifting, and dropping the shadow is what stops it reading as
   a button pasted onto the panel. */
.primary-btn {
  border-radius: 16px;
  box-shadow: none;
  font-size: 0.9375rem;
  font-weight: 500;
}
.primary-btn:hover:not(:disabled) {
  background: #232328;
  box-shadow: none;
  transform: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
   EXPANDING PLAYER CONTROL
   ───────────────────────────────────────────────────────────────────────────
   The effect from skiper-ui.com/v1/skiper3: a circular play button that
   expands into a full-width player with spring motion and glass.

   Rebuilt rather than installed. That component is React + Tailwind +
   framer-motion delivered through the shadcn registry, and ShotBorn is
   static public/ assets with no build step, no React and no Tailwind, so
   `shadcn add` has nothing to add into. It is also a paid library. This is an
   equivalent written from the described behaviour.

   Presentation only. The existing button keeps its id, its click handler and
   its .is-playing state; app.js is untouched apart from one additive block
   that publishes playback progress as a custom property.
   ═══════════════════════════════════════════════════════════════════════════ */

.image-lightbox-playpause {
  /* Collapsed: unchanged 64px circle. Width and radius are what animate. */
  width: 64px;
  overflow: hidden;
  white-space: nowrap;
  gap: 0;
  padding: 0;
  /* Glass, per the reference. saturate keeps the video's colour alive behind
     the panel instead of going grey. */
  background: rgba(18, 18, 20, 0.55);
  -webkit-backdrop-filter: blur(20px) saturate(150%);
  backdrop-filter: blur(20px) saturate(150%);
  border: 1px solid rgba(255, 255, 255, 0.22);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255,255,255,0.14);
  /* Spring. A back-out curve overshoots slightly and settles, which is what
     reads as spring without a physics library. */
  transition:
    width 520ms cubic-bezier(0.34, 1.42, 0.64, 1),
    border-radius 520ms cubic-bezier(0.34, 1.42, 0.64, 1),
    padding 520ms cubic-bezier(0.34, 1.42, 0.64, 1),
    gap 520ms cubic-bezier(0.34, 1.42, 0.64, 1),
    background-color 200ms ease;
}

/* Expanded on hover, on keyboard focus, and while playing, so it is reachable
   without a pointer and stays open while it is doing something. */
.image-lightbox-playpause:hover,
.image-lightbox-playpause:focus-visible,
.image-lightbox-playpause.is-playing {
  width: 300px;
  border-radius: 34px;
  padding: 0 22px 0 18px;
  gap: 16px;
  justify-content: flex-start;
}

/* The glyph must not be squeezed by the flex row as the pill grows, and it
   has to lead. ::after is generated last in flex order, so without order:-1
   the play triangle would sit after the track. */
.image-lightbox-playpause::after { flex: none; order: -1; }

/* The track only exists once expanded. It is inert until then, so a collapsed
   button is still just a button. */
.image-lightbox-playpause .lbp-track {
  flex: 1 1 auto;
  height: 4px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.24);
  opacity: 0;
  transition: opacity 220ms ease 120ms;
  overflow: hidden;
}
.image-lightbox-playpause .lbp-time {
  flex: none;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.9);
  opacity: 0;
  transition: opacity 220ms ease 120ms;
}
.image-lightbox-playpause:hover .lbp-track,
.image-lightbox-playpause:focus-visible .lbp-track,
.image-lightbox-playpause.is-playing .lbp-track,
.image-lightbox-playpause:hover .lbp-time,
.image-lightbox-playpause:focus-visible .lbp-time,
.image-lightbox-playpause.is-playing .lbp-time { opacity: 1; }

/* Progress is published by app.js as --lbp-progress, 0 to 1. */
.image-lightbox-playpause .lbp-fill {
  display: block; height: 100%; width: 100%;
  background: #fff; border-radius: 999px;
  transform: scaleX(var(--lbp-progress, 0));
  transform-origin: left center;
}

.image-lightbox-playpause:hover { background: rgba(26, 26, 30, 0.66); }

@media (prefers-reduced-motion: reduce) {
  .image-lightbox-playpause,
  .image-lightbox-playpause .lbp-track,
  .image-lightbox-playpause .lbp-time { transition: none; }
}
/* A pill this wide would crowd a phone; it stays a circle there. */
@media (max-width: 520px) {
  .image-lightbox-playpause:hover,
  .image-lightbox-playpause:focus-visible,
  .image-lightbox-playpause.is-playing {
    width: 64px; border-radius: 50%; padding: 0; gap: 0; justify-content: center;
  }
  .image-lightbox-playpause .lbp-track,
  .image-lightbox-playpause .lbp-time { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 9 — WORKFLOW: THE NODE CANVAS
   ───────────────────────────────────────────────────────────────────────────
   Workflow was the one view with no pass at all. It shares no vocabulary with
   the other nine (no .panel, no .field, no .section-label), so every shared
   rule missed it, and after the shell inversion its light-theme values were
   stranded on a dark ground: three contrast failures, the worst at 1.51:1.

   The system is taken from app.plumar.ai, examined in the live app. Its
   organising idea, and the thing actually worth copying, is that EVERY border
   and EVERY ink level is white at a low alpha rather than a grey value. That
   is why it coheres: one colour at many opacities over one ground.

       ink        #fff / bf 75% / 8c 55% / 73 45%
       border     0f 6% / 14 8% / 24 14%
       surface    #070709 -> #0d0d10 -> #161616 -> #1a1a1a
       card       linear-gradient(145deg,#ffffff0f,#ffffff05 35%,transparent)
       shadow     0 2px 4px #0006, 0 8px 28px #00000080, inset 0 1px 0 #ffffff17
       popover    #141417 at a generous radius
       well       #0006

   ShotBorn's node system already had the right bones: --node-color per node,
   node-port-in/out, node-connect-line, a scissors layer. This dresses that
   machinery rather than replacing it. No copy and no naming is taken.
   ═══════════════════════════════════════════════════════════════════════════ */

#view-workflow {
  --wf-ink:        #ffffff;
  --wf-ink-mid:    rgba(255,255,255,0.75);
  --wf-ink-dim:    rgba(255,255,255,0.55);
  --wf-ink-muted:  rgba(255,255,255,0.45);
  --wf-line:       rgba(255,255,255,0.06);
  --wf-line-2:     rgba(255,255,255,0.09);
  --wf-line-3:     rgba(255,255,255,0.16);
  --wf-surface:    #0d0d10;
  --wf-surface-2:  #141417;
  --wf-card: linear-gradient(145deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02) 35%, transparent);
  --wf-lift: 0 2px 4px rgba(0,0,0,0.4), 0 8px 28px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.09);
}

/* ───────────────────────────── 1. THE CANVAS ──────────────────────────────
   Dots become white at low alpha, so the grid reads as surface texture on the
   dark ground rather than as dirt on it. */
#view-workflow .node-canvas {
  --dot-color: rgba(255,255,255,0.07);
  --bg: #08080A;
}

/* ─────────────────────────── 2. NODES AS CARDS ────────────────────────────
   The card recipe: a diagonal white wash over a near-black fill, a 6% white
   hairline, and a three-layer shadow whose top inset is the lit edge. */
#view-workflow .node-head {
  background: var(--wf-card), var(--wf-surface);
  border: 1px solid var(--wf-line);
  border-radius: 18px 18px 0 0;
  color: var(--wf-ink);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 11px 13px;
  /* The category hue marks the node on one edge, never on its content. */
  box-shadow: inset 2px 0 0 -1px var(--node-color, var(--wf-line-3));
}
#view-workflow .node-head-meta { color: var(--wf-ink-muted); }
#view-workflow .node-close { color: var(--wf-ink-muted); }
#view-workflow .node-close:hover { color: var(--wf-ink); background: rgba(255,255,255,0.10); }

#view-workflow .node-box {
  background: var(--wf-card), var(--wf-surface);
  border: 1px solid var(--wf-line);
  border-radius: 18px;
  box-shadow: var(--wf-lift);
}
#view-workflow .node-body { color: var(--wf-ink-mid); }
#view-workflow .node-box-empty-glyph { color: var(--wf-ink-muted); }

/* ────────────────────── 3. PORTS AS HOLLOW RINGS ──────────────────────────
   Ports are open rings in the node's own hue. A hollow ring reads as an input
   still waiting for something; filling it is what says connected. */
#view-workflow .node-port {
  background: #0B0B0D;
  border: 1.5px solid var(--node-color, var(--wf-line-3));
  color: transparent;
  box-shadow: 0 0 0 3px rgba(0,0,0,0.35);
  transition: box-shadow 160ms ease, transform 160ms ease, background-color 160ms ease;
}
#view-workflow .node-port:hover {
  transform: translateY(-50%) scale(1.18);
  box-shadow: 0 0 0 3px rgba(0,0,0,0.35),
              0 0 0 6px color-mix(in srgb, var(--node-color, #ffffff) 26%, transparent);
}
#view-workflow .node-port.connected { background: var(--node-color, var(--wf-ink)); }

/* ───────────────────────────── 4. THE PALETTE ─────────────────────────────
   A popover rather than a docked panel: deep shadow, generous radius, sitting
   over the canvas. */
#view-workflow .node-palette {
  background: var(--wf-surface-2);
  border: 1px solid var(--wf-line-2);
  border-radius: 22px;
  box-shadow: 0 16px 60px rgba(0,0,0,0.9);
  padding: 8px;
}
#view-workflow .node-palette-item { border-radius: 14px; padding: 9px 11px; }
#view-workflow .node-palette-item:hover { background: rgba(255,255,255,0.07); }
/* The icon becomes a tinted tile in the category hue. */
#view-workflow .np-icon {
  width: 28px; height: 28px; border-radius: 9px;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--node-color, #8A9099) 16%, transparent);
  color: var(--node-color, var(--wf-ink-mid));
  border: 1px solid color-mix(in srgb, var(--node-color, #8A9099) 30%, transparent);
}
#view-workflow .np-label { color: var(--wf-ink); font-size: 0.8125rem; font-weight: 500; }
#view-workflow .np-sub { color: var(--wf-ink-muted); font-size: 0.71875rem; }

/* ──────────────── 5. FLOATING CHROME, NOT DOCKED PANELS ───────────────────
   Toolbar, zoom and count become glass pills over the canvas, so the canvas
   is the subject and the controls sit on top of it. */
#view-workflow .wf-tool-dock,
#view-workflow .node-zoom-pill,
#view-workflow .node-count-pill,
#view-workflow .node-canvas-fab {
  background: rgba(20, 20, 24, 0.72);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid var(--wf-line-2);
  color: var(--wf-ink-mid);
  box-shadow: 0 8px 32px rgba(0,0,0,0.55);
}
#view-workflow .wf-tool-btn { color: var(--wf-ink-muted); border-radius: 999px; }
#view-workflow .wf-tool-btn:hover { background: rgba(255,255,255,0.09); color: var(--wf-ink); }
#view-workflow .wf-tool-btn.active { background: rgba(255,255,255,0.14); color: var(--wf-ink); }
#view-workflow .wf-tool-key { color: var(--wf-ink-muted); font-family: var(--font-mono); }
#view-workflow .node-minimap {
  background: rgba(12,12,14,0.8);
  border: 1px solid var(--wf-line-2);
  border-radius: 14px;
}

/* ─────────────────── 6. THE THREE CONTRAST FAILURES ───────────────────────
   Measured on the dark shell: wf-qa-icon 1.51, wf-quickadd-btn 1.85,
   node-canvas-hint 2.96. All three were light-theme values left behind by the
   inversion. */
#view-workflow .node-canvas-hint {
  color: var(--wf-ink-dim);
  background: rgba(255,255,255,0.05);
  border: 1px solid var(--wf-line-2);
  opacity: 1;
}
#view-workflow .wf-quickadd-btn {
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--wf-line-2);
  color: var(--wf-ink-mid);
  font-weight: 500;
}
#view-workflow .wf-quickadd-btn:hover {
  background: rgba(255,255,255,0.11);
  color: var(--wf-ink);
  border-color: var(--wf-line-3);
}
#view-workflow .wf-qa-icon { color: var(--wf-ink-mid); }

/* ─────────────────────────────── 7. WIRES ─────────────────────────────────
   Data flows output to input, so a wire carries the SOURCE node's hue and is
   drawn thin with a faint bloom. The hit area is far wider than the visible
   line, which is what makes a 1.5px wire actually clickable to cut. */
#view-workflow .node-connect-line {
  /* No stroke or stroke-width here. app.js emits both as presentation
     attributes, setting the stroke to the SOURCE node's colour so a wire says
     where its data came from. CSS beats a presentation attribute, so declaring
     stroke here would override every wire with one value — and --node-color is
     undefined on this path anyway, because the connection SVG lives outside
     the nodes, so it would have fallen back to a dim grey for all of them. */
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.18));
}
/* No override here. The hit path is already emitted with stroke="transparent"
   stroke-width="18" pointer-events="stroke" in app.js, and CSS beats a
   presentation attribute, so setting 16 here made the target SMALLER. */

@media (prefers-reduced-motion: reduce) {
  #view-workflow .node-port { transition: none; }
}

/* Two of my own making. The rule above carries id specificity, so it beat
   `.wf-tool-btn.active .wf-tool-key`, which deliberately puts DARK ink on a
   white chip: that turned it white-on-white at 1.00:1. The active key keeps
   its dark ink. node-zoom-caret was a light-theme value never covered. */
#view-workflow .wf-tool-btn.active .wf-tool-key { color: #2a2a2e; }
#view-workflow .node-zoom-caret { color: var(--wf-ink-muted); }
/* The base key chip was still background: var(--surface-2), a light silver
   value, so white ink on it measured 1.06:1. The chip goes dark; the active
   state keeps its inverted white chip with dark ink. */
#view-workflow .wf-tool-key {
  background: rgba(255,255,255,0.10);
  border-color: rgba(255,255,255,0.14);
}
#view-workflow .wf-tool-btn.active .wf-tool-key {
  background: #fff; border-color: #fff; color: #2a2a2e;
}

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 10 — THE NODES THEMSELVES
   ───────────────────────────────────────────────────────────────────────────
   Pass 9 recoloured the node. It did not change the node. This does.

   Honest limit stated up front: ShotBorn's generator node is RICHER than
   the reference's. It carries model, aspect ratio, resolution, duration,
   sound, moodboard, an auto-inferred route badge and a cost, none of which
   exist on the reference's node. So its anatomy cannot be copied outright.
   What is copied is the TREATMENT:

     - a category eyebrow in the node's own hue, mono and tracked
     - a subtitle under the title, the thing that made theirs read as a card
       rather than a widget
     - metadata as outlined chips instead of loose inline text
     - controls that recede until wanted
     - the hue on the eyebrow, the dot, the port rings and the wire, and
       nowhere else

   The hue itself was the blocker: image and video were both var(--accent),
   which is pure black. On the dark canvas every node was the same dead grey.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─────────────────── 1. THE LABEL ROW BECOMES AN EYEBROW ──────────────────
   This row was inline body text at mixed weights. It becomes the node's
   identity line: mono, tracked, uppercase, led by the type in its own hue. */
#view-workflow .node-label-row {
  font-family: var(--font-mono);
  font-size: 0.59375rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--wf-ink-muted);
  gap: 8px;
  padding: 10px 12px 8px;
  border-bottom: 1px solid var(--wf-line);
}
#view-workflow .nlr-type {
  color: var(--node-color, var(--wf-ink));
  font-weight: 600;
}
#view-workflow .nlr-icon {
  color: var(--node-color, var(--wf-ink-mid));
  font-size: 0.75rem;
}

/* ─────────────────────── 2. METADATA AS CHIPS ─────────────────────────────
   The model name, the inferred route and the cost were loose text sitting in
   a row. As outlined chips they read as readouts, and the cost reads as a
   price rather than as another label. */
#view-workflow .nlr-model,
#view-workflow .nlr-inferred-mode,
#view-workflow .nlr-cost {
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.08em;
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--wf-line-2);
  background: rgba(255, 255, 255, 0.04);
  color: var(--wf-ink-dim);
  white-space: nowrap;
}
#view-workflow .nlr-inferred-mode {
  border-color: color-mix(in srgb, var(--node-color, #ffffff) 34%, transparent);
  color: var(--node-color, var(--wf-ink-mid));
  background: color-mix(in srgb, var(--node-color, #ffffff) 12%, transparent);
}
#view-workflow .nlr-cost {
  color: var(--wf-ink-mid);
  font-variant-numeric: tabular-nums;
}
#view-workflow .nlr-model:empty,
#view-workflow .nlr-inferred-mode:empty { display: none; }

/* ───────────────────── 3. THE TOOLBAR RECEDES ─────────────────────────────
   Selects were opaque light chips on a dark card, which made the controls the
   loudest thing on a node whose subject is the image. They become wells. */
#view-workflow .node-toolbar {
  background: var(--wf-card), var(--wf-surface);
  border: 1px solid var(--wf-line);
  border-radius: 16px;
  box-shadow: var(--wf-lift);
  gap: 6px;
  padding: 7px 9px;
}
#view-workflow .tb-select,
#view-workflow .wf-moodboard-btn,
#view-workflow .tb-icon-btn {
  background: rgba(0, 0, 0, 0.4);
  border: 1px solid var(--wf-line-2);
  color: var(--wf-ink-mid);
  border-radius: 9px;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.04em;
  transition: background-color 160ms ease, border-color 160ms ease;
}
#view-workflow .tb-select:hover,
#view-workflow .wf-moodboard-btn:hover,
#view-workflow .tb-icon-btn:hover {
  background: rgba(255, 255, 255, 0.08);
  border-color: var(--wf-line-3);
  color: var(--wf-ink);
}
#view-workflow .tb-icon { color: var(--node-color, var(--wf-ink-mid)); }
#view-workflow .wf-mb-prefix { color: var(--wf-ink-muted); }

/* The node name is the title of the card, so it stops looking like a form
   field and only reveals itself as editable on hover. */
#view-workflow .tb-name,
#view-workflow .node-head-name {
  background: transparent;
  border: 1px solid transparent;
  color: var(--wf-ink);
  font-family: var(--font-ui);
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  border-radius: 8px;
  padding: 3px 6px;
}
#view-workflow .tb-name:hover,
#view-workflow .node-head-name:hover {
  background: rgba(255, 255, 255, 0.06);
  border-color: var(--wf-line-2);
}
#view-workflow .tb-name:focus,
#view-workflow .node-head-name:focus {
  background: rgba(0, 0, 0, 0.5);
  border-color: var(--node-color, var(--wf-line-3));
  outline: none;
}

/* ───────────────────────── 4. THE MEDIA BOX ───────────────────────────────
   The subject of a generator node. It becomes a well cut into the card, so
   the image sits IN the node rather than on top of it. */
#view-workflow .node-box {
  background: rgba(0, 0, 0, 0.45);
  border: 1px solid var(--wf-line);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.6);
}
#view-workflow .node-result,
#view-workflow .wf-result {
  background: rgba(0, 0, 0, 0.4);
  border: 1px dashed var(--wf-line-3);
  border-radius: 14px;
  color: var(--wf-ink-muted);
}
#view-workflow .node-result-empty { color: var(--wf-ink-muted); font-size: 1.125rem; }

/* The prompt field inside a node is a well, not a raised input. */
#view-workflow .wf-prompt,
#view-workflow .file-drop {
  background: rgba(0, 0, 0, 0.4);
  border: 1px solid var(--wf-line-2);
  border-radius: 14px;
  color: var(--wf-ink);
  font-size: 0.8125rem;
  font-weight: 350;
}
#view-workflow .wf-prompt::placeholder { color: var(--wf-ink-muted); }
#view-workflow .wf-prompt:focus {
  border-color: var(--node-color, var(--wf-line-3));
  background: rgba(0, 0, 0, 0.55);
  outline: none;
}
#view-workflow .wf-file-label { color: var(--wf-ink-dim); font-size: 0.75rem; }

/* ─────────────────────── 5. ACTIONS ON THE NODE ───────────────────────────
   The reference keeps a node's own action modest: it is one node in a graph,
   not the page's primary button. */
#view-workflow .node-generate-btn {
  background: var(--node-color, #ffffff);
  color: #0A0A0C;
  border: none;
  border-radius: 999px;
  font-family: var(--font-ui);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  box-shadow: none;
}
#view-workflow .node-generate-btn:hover:not(:disabled) { filter: brightness(1.12); }
#view-workflow .node-enhance-btn,
#view-workflow .node-enhance-undo {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--wf-line-2);
  color: var(--wf-ink-mid);
  border-radius: 999px;
  font-size: 0.6875rem;
}
#view-workflow .node-enhance-btn:hover { background: rgba(255,255,255,0.11); color: var(--wf-ink); }
#view-workflow .node-batch-stepper {
  background: rgba(0, 0, 0, 0.4);
  border: 1px solid var(--wf-line-2);
  border-radius: 999px;
  color: var(--wf-ink-mid);
  font-family: var(--font-mono);
  font-size: 0.625rem;
}

/* ─────────────── 6. THE DOT CARRIES THE HUE IT WAS GIVEN ──────────────────
   It already read var(--node-color); that variable was just black. */
#view-workflow .node-dot {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--node-color, #ffffff) 22%, transparent);
}
/* .node-body still carried the light-silver chassis fill. Pass 10 set its ink
   to a light grade without touching its background, which put pale text on a
   pale box inside a dark node. The body is part of the card, not a surface of
   its own. */
#view-workflow .node-body { background: transparent; }
#view-workflow .tag-autocomplete {
  background: var(--wf-surface-2);
  border: 1px solid var(--wf-line-2);
  color: var(--wf-ink-mid);
}

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 11 — ONE SURFACE, FLOATING HEADER
   ───────────────────────────────────────────────────────────────────────────
   The output area was two nested dark boxes:

     .viewfinder-panel   dark, 28px radius, overflow hidden
       .viewfinder-head  dark bar with a border-bottom
       .viewfinder       another dark box, 14px radius, 24px padding

   A 14px-cornered dark rectangle floating inside a 28px-cornered dark
   rectangle with 24px of gap. Both the same colour, so neither edge carried
   any information, and the two radii could never agree. That is the
   misalignment: not a rounding bug, a box that should not have been there.

   The inner box stops being a surface. The panel becomes the single surface
   and the output fills it corner to corner. The header stops being a bar and
   floats over the output on a scrim, which is what keeps the frame reading as
   the subject rather than as something mounted inside a frame.
   ═══════════════════════════════════════════════════════════════════════════ */

.viewfinder-panel { position: relative; }

/* ─────────────────────── 1. THE SECOND BOX GOES ───────────────────────────
   No fill, no radius, no padding of its own. It is the panel's interior now,
   not an object sitting in it. The panel already clips to its own corners
   (overflow: hidden), so a tall result is still contained. */
.viewfinder {
  background: transparent;
  border-radius: 0;
  padding: 0;
}

/* ──────────────────────── 2. THE HEADER FLOATS ────────────────────────────
   Lifted out of flow and laid over the top of the output. The scrim fades to
   nothing, so it reads as light falling across the top of the frame rather
   than as a bar bolted to it, and it keeps the title legible over an
   arbitrarily bright result. */
.viewfinder-head {
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 3;
  border-bottom: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.62) 0%, rgba(0, 0, 0, 0.28) 55%, transparent 100%);
  padding: 13px 15px 30px;
  /* The scrim spans the full width but must not swallow clicks meant for the
     result underneath it; only the controls themselves stay interactive. */
  pointer-events: none;
}
.viewfinder-head > * { pointer-events: auto; }

/* The title becomes a quiet readout rather than a heading. */
.viewfinder-title {
  font-family: var(--font-mono);
  font-size: 0.59375rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.62);
}

/* The actions group becomes one glass pill, so a row of separate controls
   reads as a single instrument sitting on the image. */
.viewfinder-head-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 5px;
  border-radius: 999px;
  background: rgba(18, 18, 22, 0.55);
  -webkit-backdrop-filter: blur(16px) saturate(150%);
  backdrop-filter: blur(16px) saturate(150%);
  border: 1px solid rgba(255, 255, 255, 0.10);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.viewfinder-head-actions .icon-btn {
  background: transparent;
  border: 0;
  color: rgba(255, 255, 255, 0.7);
  border-radius: 999px;
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
}
.viewfinder-head-actions .icon-btn:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
}
.viewfinder-head-actions .icon-btn:disabled { opacity: 0.35; }

.viewfinder-tag {
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.10);
  color: rgba(255, 255, 255, 0.8);
  border: 0;
}

/* ───────────────── 3. CONTENT CLEARS THE FLOATING HEADER ──────────────────
   With the header out of flow, anything that scrolls would start underneath
   it. The empty state is centred in the whole panel and wants no offset, but
   a result grid does. */
.viewfinder.multi-result,
.viewfinder-multi { padding-top: 52px; }

/* The empty plate now centres in the full panel rather than in the leftover
   space below a bar, so it no longer sits visibly low. */
.viewfinder-empty { margin: 0 auto; }

/* ─────────────────────── 4. THE PANEL'S OWN EDGE ──────────────────────────
   With only one surface left, its edge does real work, so it gets a single
   lit hairline rather than competing borders. */
.viewfinder-panel {
  background: linear-gradient(180deg, var(--vf-surface-2), var(--vf-bg));
}

/* ═══════════════════════════════════════════════════════════════════════════
   PASS 12 — THE NODE IS ONE CARD
   ───────────────────────────────────────────────────────────────────────────
   The node was never a card. `.node` is a bare positioned wrapper with no
   background, no border and no radius, so the thing on screen was three
   detached objects that happened to be stacked:

     .node-toolbar     its own pill, own border, own shadow, margin-bottom:10px
                       so it floated ABOVE the node entirely
     .node-label-row   a second strip, revealed on selection
     .node-box         its own border and radius, a box inside nothing
     .wf-prompt        its own border again, a sub-section inside that

   In the reference every one of those lives inside a single bounded card, and
   there are no boxes within the box. So: the wrapper becomes the card, and
   every piece inside it gives up its own frame. Rows are separated by
   hairlines where a separation is real, and by nothing where it is not.

   NOT clipped with overflow:hidden. The ports are absolutely positioned to
   straddle the node's left and right edges; clipping to the radius would cut
   them in half.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─────────────────────── 1. THE WRAPPER BECOMES THE CARD ──────────────────*/
#view-workflow .node {
  background: var(--wf-card), var(--wf-surface);
  border: 1px solid var(--wf-line-2);
  border-radius: 20px;
  box-shadow: var(--wf-lift);
}
/* Selection lifts the card and lights its edge in the node's own hue, instead
   of recolouring a border on an inner box. */
#view-workflow .node.selected {
  border-color: color-mix(in srgb, var(--node-color, #ffffff) 55%, transparent);
  box-shadow: var(--wf-lift),
              0 0 0 1px color-mix(in srgb, var(--node-color, #ffffff) 35%, transparent),
              0 0 28px -6px color-mix(in srgb, var(--node-color, #ffffff) 30%, transparent);
}
#view-workflow .node.selected .node-box { border-color: transparent; }

/* ──────────────── 2. THE TOOLBAR COMES INSIDE AND FLATTENS ────────────────
   It was a floating pill sitting above the node. It becomes the card's header
   row: no fill, no border, no shadow, no pill radius, no margin pushing it
   away from the body it belongs to. */
#view-workflow .node-toolbar {
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--wf-line);
  border-radius: 20px 20px 0 0;
  box-shadow: none;
  margin-bottom: 0;
  height: auto;
  min-height: 40px;
  padding: 8px 12px;
}

/* The label row is the second half of that header, so it sits directly under
   it with no gap and only a hairline beneath. */
#view-workflow .node-label-row {
  padding: 8px 12px;
  border-bottom: 1px solid var(--wf-line);
  margin: 0;
}

/* ───────────────── 3. NO BOXES INSIDE THE BOX ─────────────────────────────
   The media area and the prompt field stop drawing their own frames. They are
   regions of the card, not objects placed on it. */
#view-workflow .node-box {
  border: 0;
  border-radius: 0;
  background: rgba(0, 0, 0, 0.32);
  box-shadow: none;
}
#view-workflow .node-body {
  padding: 0;
  border: 0;
  background: transparent;
}
#view-workflow .wf-prompt,
#view-workflow .node-box-prompt {
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  padding: 12px 13px;
  resize: none;
}
#view-workflow .wf-prompt:focus,
#view-workflow .node-box-prompt:focus {
  outline: none;
  background: rgba(255, 255, 255, 0.03);
  box-shadow: none;
}
/* The drop target keeps a dashed edge, because dashed means "put something
   here" and that is information rather than decoration. */
#view-workflow .file-drop {
  border: 1px dashed var(--wf-line-3);
  border-radius: 12px;
  background: transparent;
  margin: 10px 12px;
}
#view-workflow .node-result,
#view-workflow .wf-result {
  border: 0;
  border-radius: 0;
  background: transparent;
}

/* ───────────────────── 4. THE FOOTER IS PART OF THE CARD ──────────────────
   Batch stepper and the action sat loose at the bottom edge. They become a
   real footer row, separated by a hairline the same way the header is. */
#view-workflow .node-footer,
#view-workflow .node-actions,
#view-workflow .node-batch-row {
  border-top: 1px solid var(--wf-line);
  padding: 9px 12px;
  margin: 0;
}

/* The last region in the card carries the bottom corners so the fill never
   squares off against the card's own radius. */
#view-workflow .node > *:last-child { border-radius: 0 0 19px 19px; }

/* ────────────────────── 5. PORTS STAY ON THE EDGE ─────────────────────────
   They straddle the card boundary, which is why the card is not clipped. The
   ring gets a dark halo so it reads against both the card and the canvas. */
#view-workflow .node-port {
  z-index: 4;
  box-shadow: 0 0 0 3px var(--wf-surface), 0 0 0 4px rgba(0,0,0,0.5);
}
#view-workflow .node-port:hover {
  box-shadow: 0 0 0 3px var(--wf-surface), 0 0 0 4px rgba(0,0,0,0.5),
              0 0 0 8px color-mix(in srgb, var(--node-color, #ffffff) 24%, transparent);
}

/* No box inside the box. .node-box was still painting rgba(0,0,0,0.32), a
   visibly darker rectangle sitting inside the card, which is the nested-box
   look even with its border already removed. The media area is a REGION of
   the card, so it carries no fill of its own; the row hairlines are the only
   thing left describing the layout. */
#view-workflow .node-box,
#view-workflow .node-box-inner,
#view-workflow .node-box-media {
  background: transparent;
  box-shadow: none;
  border: 0;
}
/* Row separators stay, at the faintest useful weight: they are the layout. */
#view-workflow .node-toolbar,
#view-workflow .node-label-row { border-bottom-color: rgba(255, 255, 255, 0.05); }

/* ─────────────── CONTROLS AS LABELLED ROWS, NOT A CRAMPED STRIP ───────────
   The toolbar was a horizontal run of unlabelled selects that only made sense
   if you already knew what each one was, and at 300px it had to either scroll
   or force the node wider. Each control is a row now: a mono label above a
   full-width control, stacked. Roomier, self-describing, and it fits the
   node's own width instead of fighting it. */
#view-workflow .node-toolbar .tb-controls {
  flex-direction: column;
  align-items: stretch;
  gap: 9px;
  width: 100%;
  margin-top: 2px;
}
#view-workflow .node-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
#view-workflow .node-field-label {
  font-family: var(--font-mono);
  font-size: 0.53125rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--wf-ink-muted);
}
#view-workflow .node-field > .tb-select,
#view-workflow .node-field > .wf-moodboard-btn,
#view-workflow .node-field > .tb-icon-btn {
  width: 100%;
  max-width: none;
  height: 30px;
  text-align: left;
}
#view-workflow .node-field > .wf-moodboard-btn {
  display: flex; align-items: center; justify-content: space-between;
}
/* A hidden control must take its label with it, or the row leaves an orphan
   heading behind. Resolution is hidden until a model that supports it loads. */
#view-workflow .node-field:has(> .hidden) { display: none; }

/* The header line is just the icon and the name now that the controls have
   moved below it. */
#view-workflow .node-toolbar { align-items: flex-start; flex-direction: column; }
#view-workflow .node-toolbar > .tb-icon,
#view-workflow .node-toolbar > .tb-name { align-self: auto; }


/* ═══════════════════════════════════════════════════════════════════════════
   CONTROLS AS ONE BOTTOM STRIP
   ───────────────────────────────────────────────────────────────────────────
   Undoing my own detours. The controls went from a header strip, to stacked
   labelled rows, to a glass overlay, to a two-column grid. All of that changed
   the node's SHAPE, which was never asked for.

   The shape stays. The card is a preview area with a single compact strip of
   small pills along the bottom, the way the reference does it: stepper, the
   presets as short self-describing pills, and the run button at the right.
   No labels above controls, because "16:9" and "Auto" already say what they
   are. Always present, so selection cannot resize anything.
   ═══════════════════════════════════════════════════════════════════════════ */

#view-workflow .node { display: flex; flex-direction: column; }
#view-workflow .node .node-body { order: 1; flex: 1 1 auto; min-height: 0; }
/* Back into flow, at the bottom, with no panel of its own. */
#view-workflow .node .node-overlay,
#view-workflow .node.selected .node-overlay {
  display: block;
  position: static;
  order: 2;
  max-height: none;
  overflow: visible;
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: none;
  border: 0;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 0 0 19px 19px;
}
#view-workflow .node .node-overlay .node-toolbar,
#view-workflow .node.selected .node-overlay .node-toolbar {
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  position: static;
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 8px 10px;
  min-height: 0;
}
/* The name leads the strip; the presets follow it inline. */
#view-workflow .node .node-overlay .tb-name { flex: 0 1 auto; font-size: 0.75rem; }
#view-workflow .node .node-overlay .tb-controls {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  width: auto;
  flex: 1 1 auto;
  margin: 0;
}
/* Values are self-describing, so the labels go. */
#view-workflow .node .node-overlay .node-field-label { display: none; }
#view-workflow .node .node-overlay .node-field {
  display: inline-flex;
  flex-direction: row;
  flex: 0 0 auto;
  grid-column: auto;
}
#view-workflow .node .node-overlay .node-field > .tb-select,
#view-workflow .node .node-overlay .node-field > .wf-moodboard-btn,
#view-workflow .node .node-overlay .node-field > .tb-icon-btn {
  width: auto;
  height: 26px;
  font-size: 0.625rem;
  padding: 0 9px;
  border-radius: 999px;
}
/* The chip row is metadata, not controls: one quiet line under the strip. */
#view-workflow .node .node-overlay .node-label-row,
#view-workflow .node.selected .node-overlay .node-label-row {
  display: flex;
  position: static;
  flex-wrap: wrap;
  gap: 5px;
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 0 10px 8px;
}

/* Padding must not depend on selection or the card changes height by that
   difference. Earlier .selected-scoped paddings were still winning and were
   6px tighter than the unselected ones, which is exactly the resize this was
   meant to eliminate. Both states, one value. */
#view-workflow .node .node-overlay .node-toolbar,
#view-workflow .node.selected .node-overlay .node-toolbar { padding: 8px 10px; }
#view-workflow .node .node-overlay .node-label-row,
#view-workflow .node.selected .node-overlay .node-label-row { padding: 0 10px 8px; }
#view-workflow .node .node-overlay .tb-controls,
#view-workflow .node.selected .node-overlay .tb-controls { gap: 6px; }

/* The redundant chips were hidden only under .selected, so an UNSELECTED node
   rendered extra content and was taller: selecting it shrank the card by 4px
   (image) and 32px (video). Nothing about the node's content may depend on
   selection. These are hidden in both states, for the same reason as before:
   the strip above already shows the model, and the card already shows its own
   name and icon. */
#view-workflow .node .node-overlay .nlr-icon,
#view-workflow .node .node-overlay .nlr-type,
#view-workflow .node .node-overlay .nlr-model,
#view-workflow .node.selected .node-overlay .nlr-icon,
#view-workflow .node.selected .node-overlay .nlr-type,
#view-workflow .node.selected .node-overlay .nlr-model { display: none; }
#view-workflow .node .node-overlay .node-close,
#view-workflow .node.selected .node-overlay .node-close { margin-left: auto; }
#view-workflow .node .node-overlay .tb-name,
#view-workflow .node.selected .node-overlay .tb-name { font-size: 0.75rem; padding: 1px 5px; }
#view-workflow .node .node-overlay .nlr-inferred-mode,
#view-workflow .node .node-overlay .nlr-cost,
#view-workflow .node.selected .node-overlay .nlr-inferred-mode,
#view-workflow .node.selected .node-overlay .nlr-cost { font-size: 0.5rem; padding: 2px 6px; }

/* ─────────────────── THE STRIP IS ONE ROW ─────────────────────────────────
   The card is the aspect-ratio preview plus a single control row. The strip
   was four stacked lines because .node-toolbar and .node-label-row are
   separate blocks, each wrapping their own contents. display:contents drops
   both wrappers out of the box tree so their children become direct flex
   items of one row, with no markup change.

   Only the controls survive in it: the icon, the name and the chips repeat
   what the card already shows, and the run button and batch stepper already
   sit on the preview itself. */
#view-workflow .node .node-overlay,
#view-workflow .node.selected .node-overlay {
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: nowrap;
  gap: 6px;
  padding: 7px 9px;
  overflow-x: auto;
  scrollbar-width: none;
}
#view-workflow .node .node-overlay::-webkit-scrollbar { display: none; }
#view-workflow .node .node-overlay .node-toolbar,
#view-workflow .node.selected .node-overlay .node-toolbar,
#view-workflow .node .node-overlay .node-label-row,
#view-workflow .node.selected .node-overlay .node-label-row,
#view-workflow .node .node-overlay .tb-controls,
#view-workflow .node.selected .node-overlay .tb-controls {
  display: contents;
}
/* Repeats of what the card already says. */
#view-workflow .node .node-overlay .tb-icon,
#view-workflow .node .node-overlay .tb-name,
#view-workflow .node .node-overlay .nlr-inferred-mode,
#view-workflow .node .node-overlay .nlr-cost { display: none; }

#view-workflow .node .node-overlay .node-field { flex: 0 0 auto; }
#view-workflow .node .node-overlay .node-field > .tb-select,
#view-workflow .node .node-overlay .node-field > .wf-moodboard-btn,
#view-workflow .node .node-overlay .node-field > .tb-icon-btn {
  height: 24px;
  font-size: 0.59375rem;
  padding: 0 8px;
  border-radius: 999px;
  width: auto;
}
#view-workflow .node .node-overlay .node-close {
  margin-left: auto;
  flex: 0 0 auto;
  width: 20px; height: 20px;
  font-size: 0.6875rem;
}

/* ═══════════════════════════════════════════════════════════════════════════
   NODE: TOP ROW, PREVIEW, BOTTOM ROW
   ───────────────────────────────────────────────────────────────────────────
   The selectors were nested inside the toolbar, so no amount of CSS could put
   them below the preview. app.js now emits three siblings: .node-toprow with
   the identity and metadata, the preview, and .node-botrow with the selectors.

   No dividers between the rows: the card is one surface, and a rule across it
   would cut it into the stacked boxes this spent several passes removing. No
   dark fills on the selectors either - they sit ON the card, so a darker fill
   reads as another well punched into it. A hairline and the label is enough.
   ═══════════════════════════════════════════════════════════════════════════ */

#view-workflow .node { display: flex; flex-direction: column; }
#view-workflow .node .node-toprow { order: 0; }
#view-workflow .node .node-box     { order: 1; }
#view-workflow .node .node-botrow  { order: 2; }
#view-workflow .node .node-status-line { order: 3; }

/* Both rows: one line, no rule, no fill of their own. */
#view-workflow .node .node-toprow,
#view-workflow .node.selected .node-toprow,
#view-workflow .node .node-botrow,
#view-workflow .node.selected .node-botrow {
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: nowrap;
  gap: 7px;
  padding: 8px 11px;
  min-height: 0;
  background: transparent;
  border: 0;
  box-shadow: none;
  border-radius: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
#view-workflow .node .node-toprow::-webkit-scrollbar,
#view-workflow .node .node-botrow::-webkit-scrollbar { display: none; }

/* The metadata chips join the top row rather than forming a line of their own. */
#view-workflow .node .node-toprow .node-label-row,
#view-workflow .node.selected .node-toprow .node-label-row { display: contents; }

/* The control row is a real, self-scrolling flex track that takes whatever
   width is left after the Generate/Cancel buttons. It used to be
   display:contents, so model/aspect/moodboard competed as flex items with the
   buttons and, whenever Cancel appeared, the extra width pushed Generate off
   the node's right edge (clipped, unclickable). Now the controls shrink and
   scroll inside their own track (min-width:0), and the buttons — being fixed-
   size flex:0 siblings — are always fully on-card. */
#view-workflow .node .node-botrow .tb-controls,
#view-workflow .node.selected .node-botrow .tb-controls {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
  align-items: center;
  gap: 7px;
  overflow-x: auto;
  scrollbar-width: none;
}
#view-workflow .node .node-botrow .tb-controls::-webkit-scrollbar { display: none; }

/* Redundant with the card itself. */
#view-workflow .node .node-toprow .nlr-icon,
#view-workflow .node .node-toprow .nlr-type,
#view-workflow .node .node-toprow .nlr-model { display: none; }

#view-workflow .node .node-toprow .tb-icon { color: var(--node-color); font-size: 0.75rem; flex: 0 0 auto; }
#view-workflow .node .node-toprow .tb-name {
  flex: 1 1 auto; min-width: 0;
  font-size: 0.78125rem; font-weight: 600;
  background: transparent; border: 1px solid transparent;
  color: var(--wf-ink); padding: 2px 5px; border-radius: 7px;
}
#view-workflow .node .node-toprow .tb-name:hover { background: rgba(255,255,255,0.06); }
#view-workflow .node .node-toprow .tb-name:focus {
  background: rgba(0,0,0,0.4); border-color: var(--wf-line-3); outline: none;
}
#view-workflow .node .node-toprow .node-close { flex: 0 0 auto; margin-left: 0; }

/* Selectors: no dark fill. A hairline, transparent ground, light text. */
#view-workflow .node .node-botrow .node-field { display: contents; }
#view-workflow .node .node-botrow .node-field-label { display: none; }
#view-workflow .node .node-botrow .tb-select,
#view-workflow .node .node-botrow .wf-moodboard-btn,
#view-workflow .node .node-botrow .tb-icon-btn,
#view-workflow .node.selected .node-botrow .tb-select,
#view-workflow .node.selected .node-botrow .wf-moodboard-btn,
#view-workflow .node.selected .node-botrow .tb-icon-btn {
  flex: 0 0 auto;
  width: auto;
  height: 24px;
  padding: 0 9px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--wf-ink-mid);
  font-family: var(--font-mono);
  font-size: 0.59375rem;
  letter-spacing: 0.04em;
  box-shadow: none;
}
#view-workflow .node .node-botrow .tb-select:hover,
#view-workflow .node .node-botrow .wf-moodboard-btn:hover,
#view-workflow .node .node-botrow .tb-icon-btn:hover {
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.24);
  color: var(--wf-ink);
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE CARD IS THE ASPECT RATIO
   ───────────────────────────────────────────────────────────────────────────
   The preview was already a correct 16:9 (298x168, ratio 1.78) but the CARD
   measured 300x270, which reads square, because the two 40px rows were
   appended below it and added ~100px of height that the ratio never accounted
   for. Every extra control made the node less like its own aspect ratio.

   The rows go inside the frame, at its top and bottom edges, the way the
   reference puts them over the image. They are out of flow, so the card's
   height is the preview's height and nothing else: the node measures as its
   aspect ratio, and adding or removing a control cannot change its shape.

   Each row carries a scrim rather than a fill, so it stays readable over a
   bright generated frame without becoming another box on the card.
   ═══════════════════════════════════════════════════════════════════════════ */

#view-workflow .node .node-box { order: 0; width: 100%; }

#view-workflow .node .node-toprow,
#view-workflow .node.selected .node-toprow,
#view-workflow .node .node-botrow,
#view-workflow .node.selected .node-botrow {
  position: absolute;
  left: 0; right: 0;
  z-index: 5;
  pointer-events: none;
}
#view-workflow .node .node-toprow > *,
#view-workflow .node .node-botrow > *,
#view-workflow .node .node-toprow .node-label-row > *,
#view-workflow .node .node-botrow .tb-controls > *,
#view-workflow .node .node-botrow .node-field > * { pointer-events: auto; }

#view-workflow .node .node-toprow,
#view-workflow .node.selected .node-toprow {
  top: 0;
  padding: 9px 11px 16px;
  border-radius: 19px 19px 0 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.34) 58%, transparent 100%);
}
#view-workflow .node .node-botrow,
#view-workflow .node.selected .node-botrow {
  bottom: 0;
  padding: 16px 11px 9px;
  border-radius: 0 0 19px 19px;
  background: linear-gradient(0deg, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.36) 58%, transparent 100%);
}

/* The status line is the only thing left in flow after the frame, and it must
   not reintroduce height when empty. */
#view-workflow .node .node-status-line:empty { display: none; }

/* The frame's own contents are all absolutely pinned to its edges: the prompt
   at top:0, the enhance buttons at top:8px, the stepper and run button at
   bottom:12px. Putting the two rows over those edges landed them on top of
   each other. Everything inside the frame is inset by a row's height so the
   rows have their band and the content has the rest. Ports are deliberately
   excluded - they straddle the left and right edges, not the top or bottom. */
#view-workflow .node .node-box > .wf-prompt,
#view-workflow .node .node-box > .node-box-prompt {
  top: 44px;
  bottom: 46px;
  padding: 0 12px;
}
#view-workflow .node .node-box > .node-enhance-btn,
#view-workflow .node .node-box > .node-enhance-undo { top: 48px; }
#view-workflow .node .node-box > .node-batch-stepper,
#view-workflow .node .node-box > .node-generate-btn { bottom: 46px; }
#view-workflow .node .node-box > .node-ref-chips { bottom: 78px; }
#view-workflow .node .node-box > .node-box-empty-glyph,
#view-workflow .node .node-box > .node-box-inner {
  top: 42px;
  bottom: 44px;
}

/* Measured: the rows render 50px and 49px, but the insets were 44px and 46px,
   so the prompt overlapped the top by 6px and the controls the bottom by 3px.
   The scrim padding is trimmed so each row is ~40px, and every inset clears it
   with room to spare. Numbers chosen from the measurement, not estimated. */
#view-workflow .node .node-toprow,
#view-workflow .node.selected .node-toprow { padding: 7px 11px 9px; }
#view-workflow .node .node-botrow,
#view-workflow .node.selected .node-botrow { padding: 9px 11px 7px; }

#view-workflow .node .node-box > .wf-prompt,
#view-workflow .node .node-box > .node-box-prompt { top: 47px; bottom: 47px; }
#view-workflow .node .node-box > .node-enhance-btn,
#view-workflow .node .node-box > .node-enhance-undo { top: 50px; }
#view-workflow .node .node-box > .node-batch-stepper,
#view-workflow .node .node-box > .node-generate-btn { bottom: 48px; }
#view-workflow .node .node-box > .node-ref-chips { bottom: 82px; }
#view-workflow .node .node-box > .node-box-empty-glyph,
#view-workflow .node .node-box > .node-box-inner { top: 44px; bottom: 44px; }

/* The dropdowns were unreadable: white popup, white text. Making the closed
   select transparent left the native option list to the platform, which picks
   its palette from color-scheme - still light - while the text inherited the
   node's light ink. The select declares a dark scheme so the popup chrome
   follows, and the options carry an explicit ground and ink so they never
   depend on the platform guessing right. */
#view-workflow .node .node-botrow .tb-select,
#view-workflow .node .node-botrow select,
#view-workflow .node .node-toprow select {
  color-scheme: dark;
}
#view-workflow .node select option,
#view-workflow .node .node-botrow .tb-select option {
  background-color: #16161A;
  color: #EDEDF0;
  font-family: var(--font-ui);
  font-size: 0.75rem;
}
#view-workflow .node select option:checked,
#view-workflow .node select option:hover {
  background-color: #2A2A31;
  color: #FFFFFF;
}
/* The closed control keeps its own light ink; only the popup needed fixing. */
#view-workflow .node .node-botrow .tb-select { color: var(--wf-ink-mid); }

/* The route badge is gone. It restated what the graph already decides - a node
   with nothing wired in is text-to-X by definition - and it was the widest
   thing in the top row, which is what pushed the identity into the prompt. */
#view-workflow .node .node-toprow .nlr-inferred-mode { display: none; }

/* The enhance buttons are pinned to the frame's right edge, and the prompt
   spans the full width beneath them, so the text ran underneath. The prompt
   reserves their column. */
#view-workflow .node .node-box > .wf-prompt,
#view-workflow .node .node-box > .node-box-prompt { padding-right: 52px; }

/* Video carries five controls to Image's three, so its row ran off the right
   edge. The model name is by far the widest and the only one that can lose
   characters without losing meaning, so it takes the shrink; the short ones
   keep their full text. */
#view-workflow .node .node-botrow .wf-model {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 132px;
  text-overflow: ellipsis;
}
#view-workflow .node .node-botrow .wf-aspect,
#view-workflow .node .node-botrow .wf-resolution,
#view-workflow .node .node-botrow .wf-duration,
#view-workflow .node .node-botrow .wf-sound,
#view-workflow .node .node-botrow .wf-moodboard-btn { flex: 0 0 auto; }

/* Video's five controls needed 333px in a 318px row, which pushed Duration off
   the right edge and out of the card. Measured widths were model 20 (collapsed
   to nothing by min-width:0), aspect 66, resolution 66, duration 60, sound 71.

   Budget for a 320px node: 318 interior, minus four 6px gaps, leaves 294.
   Sound becomes a glyph instead of the words "Sound on" (71 -> 26), the short
   selects tighten, and the model keeps a floor so it can never collapse to an
   unreadable sliver again. */
#view-workflow .node .node-botrow { gap: 6px; }
#view-workflow .node .node-botrow .tb-select,
#view-workflow .node .node-botrow .wf-moodboard-btn,
#view-workflow .node .node-botrow .tb-icon-btn {
  padding: 0 6px;
  font-size: 0.5625rem;
}
#view-workflow .node .node-botrow .wf-model {
  flex: 1 1 auto;
  min-width: 84px;
  max-width: 104px;
}
#view-workflow .node .node-botrow .wf-aspect,
#view-workflow .node .node-botrow .wf-resolution { max-width: 54px; }
#view-workflow .node .node-botrow .wf-duration { max-width: 50px; }

/* The sound toggle carried the longest label in the row for a binary state.
   A glyph says the same thing: lit for on, dimmed for off. */
#view-workflow .node .node-botrow .wf-sound {
  width: 26px;
  min-width: 26px;
  padding: 0;
  font-size: 0;
  justify-content: center;
}
#view-workflow .node .node-botrow .wf-sound::before {
  content: "\266B";
  font-size: 0.6875rem;
  line-height: 1;
}
#view-workflow .node .node-botrow .wf-sound:not(.active) { opacity: 0.45; }

/* Three fixes.

   1. The model select collapsed to a bare caret when a model was chosen and
      the others pushed past it. flex-shrink was still able to take it below
      its floor; the basis is now the floor itself so it cannot be squeezed
      out no matter how many controls share the row.
   2. Enhance and undo were pinned inside the frame at top-right, sitting over
      the prompt. Removing the route badge freed room in the top row, which is
      where they belong; they are emitted there now.
   3. The centre diamond is gone. It was a placeholder glyph in the middle of
      the frame with the prompt wrapping around it. */
#view-workflow .node .node-botrow .wf-model {
  flex: 1 1 84px;
  min-width: 84px;
  max-width: 112px;
}

#view-workflow .node .node-toprow .node-enhance-btn,
#view-workflow .node .node-toprow .node-enhance-undo {
  position: static;
  flex: 0 0 auto;
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  border-radius: 999px;
  background: rgba(255,255,255,0.07);
  border: 1px solid rgba(255,255,255,0.14);
  color: var(--wf-ink-mid);
  font-size: 0.625rem;
}
#view-workflow .node .node-toprow .node-enhance-btn:hover,
#view-workflow .node .node-toprow .node-enhance-undo:hover {
  background: rgba(255,255,255,0.14); color: var(--wf-ink);
}
#view-workflow .node .node-toprow .node-enhance-undo[hidden] { display: none; }

/* The prompt no longer has to dodge them. */
#view-workflow .node .node-box > .wf-prompt,
#view-workflow .node .node-box > .node-box-prompt { padding-right: 12px; }

#view-workflow .node .node-box > .node-box-empty-glyph { display: none; }

/* The aspect select was clipping its own value: "16:" instead of "16:9", and
   the resolution likewise. 54px could not hold four characters plus the
   native caret. Widened to what the content actually needs, which the row can
   afford now that the batch stepper is gone. */
#view-workflow .node .node-botrow .wf-aspect { max-width: 68px; min-width: 62px; }
#view-workflow .node .node-botrow .wf-resolution { max-width: 66px; min-width: 60px; }
#view-workflow .node .node-botrow .wf-duration { max-width: 58px; min-width: 52px; }

/* Widening the selects to stop them clipping pushed video's row back over its
   own edge: 84+62+61+52+26 plus four 6px gaps is 309 in a 298px interior, and
   the sound glyph fell outside the card again.

   Budgeted for the 320px video node: trimming the row padding to 9px gives a
   302px interior, four 5px gaps take 20, leaving 282 for five controls. The
   sizes below total 280, so there is slack rather than a knife edge. Image
   nodes keep the roomier sizing; only video is this tight. */
#view-workflow .node-type-video .node-botrow { padding-left: 9px; padding-right: 9px; gap: 5px; }
#view-workflow .node-type-video .node-botrow .wf-model      { min-width: 78px; max-width: 190px; }
#view-workflow .node-type-video .node-botrow .wf-aspect     { min-width: 60px; max-width: 62px; }
#view-workflow .node-type-video .node-botrow .wf-resolution { min-width: 58px; max-width: 60px; }
#view-workflow .node-type-video .node-botrow .wf-duration   { min-width: 48px; max-width: 50px; }

/* Typed text was invisible. The textarea is deliberately transparent - a
   .tag-highlight-overlay paints the styled text on top of it - but I moved the
   textarea to top:47px to clear the control rows and left the overlay at
   inset:0. They were also on different type metrics (13px/18.2 vs 12px/16.8)
   and the overlay's ink was rgb(17,17,17), near-black on a near-black card, so
   even aligned it would have measured 1.08:1.

   The overlay must mirror the textarea exactly or the painted text drifts from
   the caret line by line. Same box, same metrics, light ink. */
#view-workflow .node .node-box > .tag-highlight-overlay {
  top: 47px;
  bottom: 47px;
  left: 0; right: 0;
  padding: 0 12px;
  font-family: var(--font-ui);
  font-size: 0.8125rem;
  line-height: 18.2px;
  color: var(--wf-ink);
  white-space: pre-wrap;
  word-break: break-word;
  overflow: hidden;
}
#view-workflow .node .node-box > .wf-prompt,
#view-workflow .node .node-box > .node-box-prompt {
  font-family: var(--font-ui);
  font-size: 0.8125rem;
  line-height: 18.2px;
  caret-color: var(--wf-ink);
}
/* Placeholder is the textarea's own, so it needs real ink of its own. */
#view-workflow .node .node-box > .wf-prompt::placeholder { color: rgba(255,255,255,0.38); }

/* Presets were bleeding off the card: min/max ranges let each control resize
   itself to whatever value it happened to be showing, so picking a longer
   model name grew the row and pushed the last control past the edge. Fixed
   widths, no grow, no shrink - the row's total is constant regardless of what
   is selected, and a long value ellipsises inside its own control. */
#view-workflow .node .node-botrow .tb-select,
#view-workflow .node .node-botrow .wf-moodboard-btn,
#view-workflow .node .node-botrow .tb-icon-btn { flex: 0 0 auto; }
#view-workflow .node-type-image .node-botrow .wf-model  { width: 112px; min-width: 112px; max-width: 112px; }
#view-workflow .node-type-image .node-botrow .wf-aspect { width: 64px;  min-width: 64px;  max-width: 64px; }
#view-workflow .node-type-image .node-botrow .wf-moodboard-btn { width: 62px; min-width: 62px; max-width: 62px; }
/* The model select grows to absorb the extra width the wider video node adds
   (was fixed at 82px). This keeps the generate ↑ pinned to the corner with an
   even gap to the sound ♪ — the slack goes into the model field instead of a
   lopsided gap — and gives the long model name more room. flex:1 1 auto is
   inherited from the generic .wf-model rule; only the caps needed lifting. */
#view-workflow .node-type-video .node-botrow .wf-model      { flex: 1 1 0; width: auto; min-width: 82px; max-width: 190px; }
#view-workflow .node-type-video .node-botrow .wf-aspect     { width: 62px; min-width: 62px; max-width: 62px; }
#view-workflow .node-type-video .node-botrow .wf-resolution { width: 60px; min-width: 60px; max-width: 60px; }
#view-workflow .node-type-video .node-botrow .wf-duration   { width: 50px; min-width: 50px; max-width: 50px; }

/* The Generate button was not disabled - it was buried. Growing the prompt to
   bottom:47px put it over the button at bottom:48px, so elementFromPoint at
   the button's centre returned the textarea and every click went to the text
   field instead. The action and the enhance controls sit above the prompt
   layer; the prompt keeps a clear channel so text never runs under them. */
#view-workflow .node .node-box > .node-generate-btn { z-index: 6; }
#view-workflow .node .node-box > .wf-prompt,
#view-workflow .node .node-box > .node-box-prompt,
#view-workflow .node .node-box > .tag-highlight-overlay { z-index: 1; }
/* Keep the last line of the prompt out from under the round action. */
#view-workflow .node .node-box > .wf-prompt { bottom: 56px; }
#view-workflow .node .node-box > .tag-highlight-overlay { bottom: 56px; }

/* The sound toggle kept showing its "Sound on" label next to the glyph and
   overflowed the card. My own ordering: a later .tb-icon-btn rule set
   font-size: 0.5625rem, which beat the font-size:0 the glyph substitution depends on.
   Re-asserted last, with a pinned width so it cannot grow when its label
   changes between "Sound on" and "Sound off". */
#view-workflow .node .node-botrow .wf-sound,
#view-workflow .node.selected .node-botrow .wf-sound,
#view-workflow .node-type-video .node-botrow .wf-sound {
  font-size: 0;
  width: 26px;
  min-width: 26px;
  max-width: 26px;
  padding: 0;
  flex: 0 0 26px;
  overflow: hidden;
  justify-content: center;
}
#view-workflow .node .node-botrow .wf-sound::before { font-size: 0.6875rem; }

/* The whole node showed a text I-beam. The prompt spans nearly the entire
   frame, and a textarea's cursor is `text`, so the node read as "type here"
   everywhere and felt unclickable even though dragging works. The cursor now
   states what the surface actually does: grab while the prompt is unfocused
   (a press there drags the node), a caret only once it is being edited. */
#view-workflow .node { cursor: grab; }
#view-workflow .node .wf-prompt:not(:focus),
#view-workflow .node .node-box-prompt:not(:focus),
#view-workflow .node .tag-highlight-overlay { cursor: grab; }
#view-workflow .node .wf-prompt:focus,
#view-workflow .node .node-box-prompt:focus { cursor: text; }
#view-workflow .node.dragging,
#view-workflow .node.dragging .wf-prompt { cursor: grabbing; }
/* Controls keep their own affordances. */
#view-workflow .node :is(select, button, .node-port, .tb-name) { cursor: pointer; }
#view-workflow .node .tb-name:focus { cursor: text; }
#view-workflow .node .node-port { cursor: pointer; }

/* The highlight overlay sits over the textarea, so a click could land on it
   instead of the field and the caret never appeared. It only paints text -
   it is never a target - so it passes every pointer straight through. */
#view-workflow .node .tag-highlight-overlay { pointer-events: none; }

/* Selecting a video node shifted its controls right by 2, 4, 6, 8, 10px -
   cumulative, so the left padding and every gap each grew 2px while the card
   and the controls themselves never changed size.

   A specificity collision of my own: `.node.selected .node-botrow` carries one
   more class than `.node-type-video .node-botrow`, so on selection the generic
   11px/7px beat video's tuned 9px/5px. The video rule now matches the selected
   state at equal specificity and later in the file, so both states resolve to
   the same numbers. */
#view-workflow .node.node-type-video .node-botrow,
#view-workflow .node.node-type-video.selected .node-botrow {
  padding-left: 9px;
  padding-right: 9px;
  gap: 5px;
}

/* Same collision on image nodes: a generic `.node .node-botrow { gap: 6px }`
   was being beaten on selection by the earlier `.node.selected .node-botrow`
   at gap 7px, so the model stayed put while everything after it slid one
   pixel per gap.

   One authoritative declaration for both states, then video's tighter budget
   after it - also for both states. Nothing about the row's metrics may depend
   on selection. */
#view-workflow .node .node-botrow,
#view-workflow .node.selected .node-botrow,
#view-workflow .node .node-toprow,
#view-workflow .node.selected .node-toprow {
  gap: 6px;
  padding-left: 11px;
  padding-right: 11px;
}
#view-workflow .node.node-type-video .node-botrow,
#view-workflow .node.node-type-video.selected .node-botrow {
  gap: 5px;
  padding-left: 9px;
  padding-right: 9px;
}

/* The moodboard label still changed size on selection: an earlier
   `.node.selected .node-botrow .wf-moodboard-btn` set 9.5px against the
   unselected 9px, so the label grew and the caret slid. Equalised. */
#view-workflow .node .node-botrow .wf-moodboard-btn,
#view-workflow .node.selected .node-botrow .wf-moodboard-btn,
#view-workflow .node .node-botrow .wf-mb-label,
#view-workflow .node.selected .node-botrow .wf-mb-label,
#view-workflow .node .node-botrow .node-zoom-caret,
#view-workflow .node.selected .node-botrow .node-zoom-caret {
  font-size: 0.5625rem;
  line-height: 1.5;
}

/* Node right-click menu. */
.wf-node-menu {
  position: fixed;
  z-index: 10050;
  min-width: 150px;
  padding: 5px;
  border-radius: 12px;
  background: rgba(20, 20, 24, 0.96);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  backdrop-filter: blur(20px) saturate(140%);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.7);
}
.wf-node-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 11px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: rgba(255, 255, 255, 0.86);
  font-family: var(--font-ui);
  font-size: 0.78125rem;
  cursor: pointer;
}
.wf-node-menu-item:hover { background: rgba(255, 255, 255, 0.10); color: #fff; }

/* One surface, no panel behind the text.

   The prompt textarea was already fully transparent, so it was never the box.
   .tag-highlight-overlay - the layer that paints the styled prompt text - was
   also painting linear-gradient(rgba(10,10,12,0.85)...) across the frame, and
   that darker rectangle behind the typing is what read as a box inside the
   card. It renders text now and nothing else.

   The top and bottom rows lose their scrims for the same reason: they were
   two more shaded bands on a card that is already dark. They existed to keep
   controls legible over a generated frame, but .node-box-inner is inset clear
   of both rows, so the media never sits under them and there is nothing for a
   scrim to protect against. */
#view-workflow .node .tag-highlight-overlay {
  background: none;
  background-image: none;
  /* The prompt textarea renders its own text transparent (color:transparent
     via .wf-prompt-tagwrap) and this overlay paints the visible glyphs. The
     base .tag-highlight-overlay rule colors it var(--text) — a near-black meant
     for the light app chrome — so on the dark node canvas the typed prompt was
     black-on-black and read as "no text shows unless selected". Paint it in the
     node's own light ink so it matches what .wf-prompt intends (color:var(--wf-ink)). */
  color: var(--wf-ink);
}
/* @reference tokens inside a node prompt: the base mark rule colors them
   var(--accent) (near-black in this theme), which is the same black-on-black
   problem. Keep the monochrome "weight, not color" language — light ink plus
   the mark's existing double text-shadow reads as a subtle bold on the dark
   node without a width-shifting font-weight change. */
#view-workflow .node .tag-highlight-overlay mark { color: var(--wf-ink); }
#view-workflow .node .node-toprow,
#view-workflow .node.selected .node-toprow,
#view-workflow .node .node-botrow,
#view-workflow .node.selected .node-botrow {
  background: none;
  background-image: none;
}

/* The run button joins the bottom row instead of floating over the frame.
   It was absolutely positioned at bottom-right inside .node-box; as the last
   item in .node-botrow it sits at the end of the control run, which is where
   the reference puts it. Nodes gain 36/38px so it fits without crowding the
   presets. */
#view-workflow .node .node-botrow .node-generate-btn {
  position: static;
  flex: 0 0 auto;
  margin-left: auto;
  width: 30px;
  height: 30px;
  min-width: 30px;
  padding: 0;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8125rem;
  line-height: 1;
  background: var(--node-color, #E3E9EE);
  color: #0A0A0C;
  border: 0;
  box-shadow: none;
}
#view-workflow .node .node-botrow .node-generate-btn:hover:not(:disabled) { filter: brightness(1.1); }
#view-workflow .node .node-botrow .node-generate-btn:disabled { opacity: 0.4; }

/* The prompt can use the full frame now that nothing floats in its corner. */
#view-workflow .node .node-box > .wf-prompt,
#view-workflow .node .node-box > .tag-highlight-overlay { bottom: 47px; }

/* Enhance and undo on the Prompt node sit in its header, matching the pill
   treatment they have on generator nodes. */
#view-workflow .node-type-prompt .node-head .node-enhance-btn,
#view-workflow .node-type-prompt .node-head .node-enhance-undo {
  position: static;
  flex: 0 0 auto;
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--wf-ink-mid);
  font-size: 0.625rem;
}
#view-workflow .node-type-prompt .node-head .node-enhance-btn:hover,
#view-workflow .node-type-prompt .node-head .node-enhance-undo:hover {
  background: rgba(255, 255, 255, 0.14); color: var(--wf-ink);
}
#view-workflow .node-type-prompt .node-head .node-enhance-undo[hidden] { display: none; }
#view-workflow .node-type-prompt .node-head .node-enhance-btn.is-loading { opacity: 0.55; }

/* ─────────────── RUNNING STATE ON BOTH NODE ACTIONS ───────────────────────
   Both already carry a class while working - runWorkflowNode adds .loading to
   the run button, wireEnhanceButton adds .is-loading to ✦ - but nothing styled
   .node-generate-btn.loading, so pressing Generate looked identical to not
   having pressed it. */
@keyframes wf-btn-spin { to { transform: rotate(360deg); } }

#view-workflow .node .node-botrow .node-generate-btn {
  position: relative;
  transition: background-color 160ms ease, opacity 160ms ease;
}
/* The arrow is hidden rather than removed, so the button keeps its exact size
   and the row cannot reflow the moment a generation starts. */
#view-workflow .node .node-botrow .node-generate-btn.loading {
  color: transparent;
  pointer-events: none;
}
#view-workflow .node .node-botrow .node-generate-btn.loading::after {
  content: "";
  position: absolute;
  width: 14px;
  height: 14px;
  border-radius: 999px;
  border: 2px solid rgba(10, 10, 12, 0.22);
  border-top-color: #0A0A0C;
  animation: wf-btn-spin 0.75s linear infinite;
}

/* ✦ spins while it works. The earlier rule only dimmed it to 0.55, which
   reads as disabled rather than busy. */
#view-workflow .node .node-enhance-btn.is-loading,
#view-workflow .node-type-prompt .node-head .node-enhance-btn.is-loading {
  opacity: 1;
  color: var(--wf-ink);
  border-color: rgba(255, 255, 255, 0.3);
  animation: wf-btn-spin 0.9s linear infinite;
  pointer-events: none;
}

/* Motion is the signal here, so with it suppressed the state still has to be
   readable: the run button pulses and ✦ holds a lit ring instead. */
@media (prefers-reduced-motion: reduce) {
  #view-workflow .node .node-botrow .node-generate-btn.loading::after,
  #view-workflow .node .node-enhance-btn.is-loading { animation: none; }
  #view-workflow .node .node-botrow .node-generate-btn.loading { opacity: 0.6; }
  #view-workflow .node .node-enhance-btn.is-loading {
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.35);
  }
}

/* The prompt node's output port rendered as a "D": border-radius
   0 0 19px 19px instead of 50%. My own rule - `.node > *:last-child` gets the
   card's bottom corners - was written for the last REGION of a generator node,
   where the ports sit inside .node-box and were never affected. On a prompt
   node the ports are direct children of .node, so the out port was the last
   child and got squared off. Ports are round, always. */
#view-workflow .node > .node-port:last-child,
#view-workflow .node .node-port {
  border-radius: 50%;
}

/* ─────────── PROMPT AND REFERENCE FOLLOW THE SAME PRINCIPLES ──────────────
   The generator nodes became one card with a flat top row, a content area and
   a flat bottom row, no inner surfaces. Prompt and Reference never got that:
   they still use .node-head, which pass 9 gave its own fill, border and
   bottom rule, so the header read as a band bolted to the card. Reference also
   drew the file name in its own bordered box - a box inside the box.

   Same card, same flat rows, nothing painting inside it. */
#view-workflow .node-type-prompt .node-head,
#view-workflow .node-type-reference .node-head {
  background: none;
  background-image: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 8px 11px;
  gap: 7px;
  min-height: 0;
}
/* The type dot keeps the hue; the header itself no longer needs an edge. */
#view-workflow .node-type-prompt .node-head-label,
#view-workflow .node-type-reference .node-head-label {
  color: var(--node-color);
  font-size: 0.75rem;
}
#view-workflow .node-type-prompt .node-head-name,
#view-workflow .node-type-reference .node-head-name {
  flex: 1 1 auto;
  min-width: 0;
  background: transparent;
  border: 1px solid transparent;
  color: var(--wf-ink);
  font-size: 0.78125rem;
  font-weight: 600;
  border-radius: 7px;
  padding: 2px 5px;
}
#view-workflow .node-type-prompt .node-head-name:hover,
#view-workflow .node-type-reference .node-head-name:hover { background: rgba(255,255,255,0.06); }
#view-workflow .node-type-prompt .node-head-name:focus,
#view-workflow .node-type-reference .node-head-name:focus {
  background: rgba(0,0,0,0.4); border-color: var(--wf-line-3); outline: none;
}

/* No inner surfaces in the body of either. */
#view-workflow .node-type-prompt .node-body,
#view-workflow .node-type-reference .node-body {
  background: transparent;
  border: 0;
  padding: 0 11px 10px;
}
#view-workflow .node-type-prompt .wf-prompt {
  background: transparent;
  border: 0;
  box-shadow: none;
  padding: 0;
  min-height: 84px;
  resize: none;
}
/* The file name was a bordered field. It is a label on the card now; the
   dashed drop target keeps its edge, because dashed means "put something
   here" and that is information rather than decoration. */
#view-workflow .node-type-reference .file-drop {
  background: transparent;
  border: 1px dashed var(--wf-line-3);
  border-radius: 12px;
  margin: 0 0 9px;
  padding: 10px 12px;
  box-shadow: none;
}
#view-workflow .node-type-reference .file-drop:has(+ .node-result img),
#view-workflow .node-type-reference.has-media .file-drop { border-style: solid; border-color: transparent; padding: 0 0 8px; }
#view-workflow .node-type-reference .wf-file-label {
  color: var(--wf-ink-mid);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.08em;
}
#view-workflow .node-type-reference .node-result,
#view-workflow .node-type-reference .wf-result {
  background: transparent;
  border: 0;
  border-radius: 12px;
  overflow: hidden;
}
#view-workflow .node-type-reference .node-result img { display: block; width: 100%; border-radius: 12px; }

/* The reference preview fills whatever ratio its upload set (applied inline by
   the file handler), so the node's shape follows the reference. Without
   object-fit the image would letterbox inside the ratio it just defined. */
#view-workflow .node-type-reference .wf-result { width: 100%; }
#view-workflow .node-type-reference .wf-result img,
#view-workflow .node-type-reference .wf-result video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 12px;
}

/* The run button sat 12px above the presets and clipped at the card edge. The
   base stylesheet positions .node-generate-btn with bottom:12px for when it
   floated inside the frame; adding position:relative (needed so the loading
   spinner's ::after can anchor to it) brought that offset back to life and
   shifted the button up by exactly 12px. The offsets are cleared, so relative
   positioning gives the spinner an anchor without moving the button itself. */
#view-workflow .node .node-botrow .node-generate-btn {
  top: auto;
  right: auto;
  bottom: auto;
  left: auto;
  align-self: center;
  margin-top: 0;
  margin-bottom: 0;
}

/* ─────────────────── THE CANVAS IS A RECESS, NOT A SEAM ───────────────────
   The workspace was a flat rgb(8,8,10) rectangle at radius 0, full width,
   butted straight against the page. Its top edge was the "divider": a hard
   horizontal seam with square corners and no shape of its own.

   Every other surface in this redesign is a bounded object. The canvas becomes
   one too - but a recess rather than a plate, because it is cut INTO the page
   and the work happens down inside it. Hence an inset shadow and a lit top
   hairline, the inverse of the lift a panel gets. No rule anywhere; the edge
   itself does the separating. */
#view-workflow .node-canvas-wrap {
  border-radius: 20px;
  overflow: hidden;
  box-shadow:
    inset 0 1px 3px rgba(0, 0, 0, 0.9),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    0 1px 0 rgba(255, 255, 255, 0.05);
}
#view-workflow .node-canvas {
  border-radius: 20px;
  background:
    radial-gradient(var(--dot-color) 1.3px, transparent 1.3px) 0 0 / 22px 22px,
    radial-gradient(120% 90% at 50% -10%, rgba(120, 132, 150, 0.07), transparent 62%),
    #08080A;
}

/* ───────────────────────── HEADER: LESS PREAMBLE ──────────────────────────
   .panel-head was 150px before any work could start, most of it a two-line
   instruction. The title carries the page; the instructions become one quiet
   line at the reading grade rather than a paragraph competing with it. */
#view-workflow .panel-head { margin-bottom: 14px; }
#view-workflow .panel-head h1 { margin-bottom: 4px; }
#view-workflow .panel-head .panel-sub {
  font-size: 0.75rem;
  line-height: 1.5;
  max-width: 62ch;
  color: var(--shell-ink-faint);
}
/* Save sits with the history controls as one cluster, not three loose items. */
#view-workflow .workflow-save-row { gap: 6px; align-items: center; }

/* The palette row is chrome for the canvas, so it reads as a toolbar above it
   rather than a band of buttons floating in the page. */
#view-workflow .wf-quickadd-row { padding: 0 0 10px; gap: 7px; }

/* ─────────────────────── CANVAS TONE ──────────────────────────────────────
   The canvas was faded: the recess pass layered a
   radial-gradient(rgba(120,132,150,0.07)) haze across its top, which turned a
   black ground grey. A premium black is deep and UNIFORM - the depth comes
   from the recess edge and the dot grid, never from a wash laid over it.

   Four grounds, near-black to near-white. Nodes stay dark at every tone: a
   dark card reads cleanly on both a black and a white ground, and flipping the
   nodes too would mean re-tuning every control's contrast for four themes. */
#view-workflow .node-canvas {
  background:
    radial-gradient(var(--wf-dot) 1.3px, transparent 1.3px) 0 0 / 22px 22px,
    var(--wf-ground);
  transition: background-color 220ms ease;
}
#view-workflow .node-canvas[data-canvas-tone="onyx"]   { --wf-ground: #060608; --wf-dot: rgba(255,255,255,0.075); --wf-wire: rgba(255,255,255,0.78); }
#view-workflow .node-canvas[data-canvas-tone="slate"]  { --wf-ground: #1A1C20; --wf-dot: rgba(255,255,255,0.085); --wf-wire: rgba(255,255,255,0.80); }
#view-workflow .node-canvas[data-canvas-lum="light"] { --wf-ground: #9AA1A8; --wf-dot: rgba(10,12,16,0.16);   --wf-wire: rgba(12,14,18,0.72); }
#view-workflow .node-canvas[data-canvas-lum="light"]  { --wf-ground: #EEF1F3; --wf-dot: rgba(10,12,16,0.14);   --wf-wire: rgba(12,14,18,0.66); }
/* Fallback before the stored tone is applied. */
#view-workflow .node-canvas { --wf-ground: #060608; --wf-dot: rgba(255,255,255,0.075); --wf-wire: rgba(255,255,255,0.78); }

/* A silver wire vanishes on a light ground, so the stroke follows the tone
   rather than the node. This is the one place the attribute is overridden,
   deliberately: legibility against the ground outranks source colour. */
#view-workflow .node-canvas[data-canvas-lum="light"] .node-connect-line,
#view-workflow .node-canvas[data-canvas-lum="light"] .node-connect-line {
  stroke: var(--wf-wire);
  filter: none;
}

/* The page behind the recess was a grey gradient, which read as haze around a
   black hole. Deepened so the canvas sits in something, not on it. */
body {
  background:
    radial-gradient(120% 80% at 78% -10%, rgba(90, 100, 120, 0.09), transparent 58%),
    linear-gradient(168deg, #0C0C0E 0%, #09090B 55%, #060607 100%) !important;
}
#view-workflow .node-canvas-wrap {
  box-shadow:
    inset 0 1px 3px rgba(0, 0, 0, 0.95),
    inset 0 0 0 1px rgba(255, 255, 255, 0.055),
    0 1px 0 rgba(255, 255, 255, 0.045);
}

/* ── the control ── */
#view-workflow .wf-canvas-tone {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  border-radius: 999px;
  pointer-events: auto;
  background: rgba(20, 20, 24, 0.72);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid rgba(255, 255, 255, 0.09);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
}
#view-workflow .wf-tone {
  width: 15px; height: 15px;
  padding: 0;
  border-radius: 50%;
  cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.28);
  transition: transform 160ms ease, box-shadow 160ms ease;
}
/* Each swatch IS its ground, so the control needs no labels to be read. */
#view-workflow .wf-tone[data-tone="onyx"]   { background: #060608; }
#view-workflow .wf-tone[data-tone="slate"]  { background: #1A1C20; }
#view-workflow .wf-tone[data-tone="pewter"] { background: #9AA1A8; }
#view-workflow .wf-tone[data-tone="chalk"]  { background: #EEF1F3; }
#view-workflow .wf-tone:hover { transform: scale(1.14); }
#view-workflow .wf-tone.active {
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.85);
  border-color: transparent;
}
@media (prefers-reduced-motion: reduce) {
  #view-workflow .wf-tone, #view-workflow .node-canvas { transition: none; }
}

/* ───────────── EMPTY CANVAS: THE WAY IN SITS IN THE MIDDLE ────────────────
   The instruction and the node palette were stacked above the canvas, so an
   empty workspace showed a large blank rectangle with its controls parked off
   to one side. The palette now lives inside the canvas: centred with the hint
   while there is nothing to do, and docked to the top-left the moment there
   is, where it becomes a toolbar rather than a landing page. */
#view-workflow .node-canvas-wrap { position: relative; }

#view-workflow .wf-quickadd-row {
  position: absolute;
  z-index: 8;
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  left: 50%;
  top: 50%;
  transform: translate(-50%, calc(-50% + 26px));
  padding: 0;
  width: max-content;
  max-width: calc(100% - 40px);
  transition: none;
}
/* Once there are nodes it stops being the subject and becomes chrome. */
#view-workflow .node-canvas.has-nodes ~ .wf-quickadd-row,
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-row {
  left: 14px;
  top: 14px;
  transform: none;
  justify-content: flex-start;
}

/* The hint sits above the palette as the heading of the empty state, not as a
   pill floating in the middle of nothing. */
#view-workflow .node-canvas-hint {
  top: 50%;
  transform: translate(-50%, calc(-50% - 26px));
  background: transparent;
  border: 0;
  padding: 0;
  font-size: 0.8125rem;
  line-height: 1.6;
  text-align: center;
  white-space: normal;
  max-width: 40ch;
  color: var(--wf-ink-dim);
}
/* On the light tones the hint has to invert with the ground. */
#view-workflow .node-canvas[data-canvas-lum="light"] .node-canvas-hint,
#view-workflow .node-canvas[data-canvas-lum="light"] .node-canvas-hint {
  color: rgba(12, 14, 18, 0.66);
}
#view-workflow .node-canvas[data-canvas-lum="light"] ~ .wf-quickadd-row .wf-quickadd-btn,
#view-workflow .node-canvas[data-canvas-lum="light"] ~ .wf-quickadd-row .wf-quickadd-btn {
  background: rgba(255, 255, 255, 0.72);
  border-color: rgba(12, 14, 18, 0.16);
  color: rgba(12, 14, 18, 0.86);
}

/* Every control in .node-viewport-controls is absolutely positioned with its
   own coordinates; the tone group had none, so it fell into static flow and
   stretched across the top of the canvas. It joins the bottom-left stack,
   above the node count - the bottom-right is already the minimap and zoom. */
#view-workflow .wf-canvas-tone {
  position: absolute;
  left: 16px;
  bottom: 58px;
  z-index: 26;
  width: max-content;
}

/* ──────────── LIGHT TONES: THE FLOATING CHROME HAS TO INVERT ──────────────
   On pewter and chalk the canvas controls stayed dark-on-dark styling over a
   light ground: the palette buttons ghosted out and the zoom pill went
   light-on-light. Two of my earlier overrides also never matched at all -
   they used `.node-canvas[...] ~ .wf-quickadd-row`, but the palette was moved
   BEFORE the canvas in the DOM and a sibling combinator only looks forward.
   Scoped on the wrap with :has() instead, which does not care about order. */
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-quickadd-btn,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-quickadd-btn,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .node-zoom-pill,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .node-zoom-pill,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .node-count-pill,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .node-count-pill,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .node-canvas-fab,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .node-canvas-fab,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-tool-dock,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-tool-dock {
  background: rgba(255, 255, 255, 0.86);
  border-color: rgba(12, 14, 18, 0.14);
  color: rgba(12, 14, 18, 0.88);
  box-shadow: 0 8px 26px rgba(12, 18, 28, 0.20);
}
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-tool-btn,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-tool-btn,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .node-zoom-caret,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .node-zoom-caret,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-qa-icon,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-qa-icon {
  color: rgba(12, 14, 18, 0.72);
}
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-tool-btn.active,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-tool-btn.active {
  background: rgba(12, 14, 18, 0.10);
  color: rgba(12, 14, 18, 0.95);
}
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-tool-key,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .wf-tool-key {
  background: rgba(12, 14, 18, 0.10);
  border-color: rgba(12, 14, 18, 0.16);
  color: rgba(12, 14, 18, 0.7);
}
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .node-canvas-hint,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) .node-canvas-hint {
  color: rgba(12, 14, 18, 0.66);
}

/* The canvas showed a darker band across its top on the light tones. That was
   the recess edge: inset 0 1px 3px rgba(0,0,0,0.95), tuned for a black ground
   where it reads as depth. Over pewter or chalk the same shadow is a grey
   smudge, which sections the workspace exactly where it should be continuous.

   The edge follows the tone. Dark grounds keep the deep inset; light grounds
   get a faint dark hairline instead of a gradient, so the recess still has an
   edge without a band inside it. The workspace stays one surface either way. */
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]),
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) {
  box-shadow:
    inset 0 0 0 1px rgba(12, 18, 28, 0.14),
    0 1px 0 rgba(255, 255, 255, 0.06);
}
/* Slate is dark enough to keep depth, but not so dark that the full-strength
   inset is invisible against it. */
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-tone="slate"]) {
  box-shadow:
    inset 0 1px 3px rgba(0, 0, 0, 0.55),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    0 1px 0 rgba(255, 255, 255, 0.045);
}

/* Docked, the palette sits ON the canvas rather than on the page, and its
   buttons were rgba(255,255,255,0.06) over a #060608 ground - present in the
   DOM, visible:1, correctly positioned, and invisible to the eye. It reads as
   having disappeared because it effectively had.

   Docked it becomes canvas chrome, matching the tool dock and zoom pill: a
   glass group with a real edge. Centred on an empty canvas it stays
   understated, because there the hint above it is doing the explaining. */
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-row {
  padding: 5px;
  border-radius: 999px;
  background: rgba(20, 20, 24, 0.72);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid rgba(255, 255, 255, 0.09);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
}
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-btn {
  background: rgba(255, 255, 255, 0.10);
  border-color: rgba(255, 255, 255, 0.16);
  color: rgba(255, 255, 255, 0.88);
}
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-btn:hover {
  background: rgba(255, 255, 255, 0.18);
  color: #fff;
}
/* On the light grounds the same group inverts, like the rest of the chrome. */
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes):has(.node-canvas[data-canvas-lum="light"]) .wf-quickadd-row,
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes):has(.node-canvas[data-canvas-lum="light"]) .wf-quickadd-row {
  background: rgba(255, 255, 255, 0.86);
  border-color: rgba(12, 14, 18, 0.14);
  box-shadow: 0 8px 26px rgba(12, 18, 28, 0.20);
}
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes):has(.node-canvas[data-canvas-lum="light"]) .wf-quickadd-btn,
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes):has(.node-canvas[data-canvas-lum="light"]) .wf-quickadd-btn {
  background: rgba(12, 14, 18, 0.06);
  border-color: rgba(12, 14, 18, 0.16);
  color: rgba(12, 14, 18, 0.88);
}

/* ───────── THE PALETTE LIVES WITH THE TOOLS, NOT IN A CORNER ──────────────
   Docking it top-left created a second cluster of canvas controls in a
   different corner from the first. It belongs with the tool dock: one place
   for anything that acts on the canvas.

   Measured: the dock is 46x82 at left:16, vertically centred, on 32x32
   buttons. The palette continues that column directly beneath it - half the
   dock (41px) plus an 8px gap - and takes the same icon-only form, so the two
   read as one control surface rather than two visiting groups.

   Centred while the canvas is empty, as before; that is the one time it is
   the subject rather than a tool. */
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-row {
  left: 16px;
  top: 50%;
  transform: translateY(49px);
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: max-content;
  padding: 6px;
  border-radius: 999px;
}
/* Icon-only, matching the dock. The label survives as the accessible name and
   the tooltip, so nothing is lost by hiding the text. */
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-btn {
  width: 32px;
  height: 32px;
  min-width: 32px;
  padding: 0;
  gap: 0;
  font-size: 0;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
}
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-qa-icon {
  font-size: 0.8125rem;
  line-height: 1;
}

/* ─────────── ONE SECTION: THE TONE OWNS THE WHOLE WORKSPACE ───────────────
   The tone painted only the canvas, so a light ground sat inside a dark page
   with the title, subtitle and Save cluster stranded on the old surface. The
   workspace is one thing: the view carries the ground, the canvas contributes
   only its dot grid, and the type inverts with it.

   The recess edge goes with it - there is no longer an inner surface to cut
   into, so an edge there would be exactly the section this removes. */
#view-workflow {
  background: var(--wf-ground, #060608);
  border-radius: 20px;
  transition: background-color 220ms ease;
}
#view-workflow:has(.node-canvas[data-canvas-tone="onyx"])   { --wf-ground: #060608; }
#view-workflow:has(.node-canvas[data-canvas-tone="slate"])  { --wf-ground: #1A1C20; }
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) { --wf-ground: #9AA1A8; }
#view-workflow:has(.node-canvas[data-canvas-lum="light"])  { --wf-ground: #EEF1F3; }

/* The canvas is now part of that surface, not a panel on it. */
#view-workflow .node-canvas {
  background: radial-gradient(var(--wf-dot) 1.3px, transparent 1.3px) 0 0 / 22px 22px;
}
#view-workflow .node-canvas-wrap,
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-tone="onyx"]),
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-tone="slate"]),
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]),
#view-workflow .node-canvas-wrap:has(.node-canvas[data-canvas-lum="light"]) {
  box-shadow: none;
  border-radius: 0;
}

/* Type follows the ground it is sitting on. */
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .panel-head h1,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .panel-head h1 {
  color: #101216 !important;
}
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .panel-head .panel-sub,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .panel-head .panel-sub {
  color: rgba(16, 18, 22, 0.68) !important;
}
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .workflow-save-row button,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .workflow-save-row button,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .mobile-back-btn,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .mobile-back-btn {
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid rgba(12, 14, 18, 0.16);
  color: #101216;
  box-shadow: 0 2px 8px rgba(12, 18, 28, 0.14);
}

/* ───────────── ONE TOOLBAR, NOT TWO STACKED PILLS ────────────────────────
   The tools and the palette were two separate rounded groups with a gap: two
   toolbars visiting the same corner. They are one control surface, so they
   join into a single column - the dock keeps the top cap, the palette the
   bottom, and the seam between them is a hairline rather than a gap.

   The dock is 82px tall and vertically centred, so its bottom edge is exactly
   50% + 41px; the palette starts there with no offset. */
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) ~ #node-viewport-controls .wf-tool-dock,
#view-workflow #node-viewport-controls .wf-tool-dock {
  border-radius: 22px 22px 0 0;
  border-bottom: 0;
  padding-bottom: 3px;
}
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-row {
  transform: translateY(41px);
  border-radius: 0 0 22px 22px;
  border-top: 1px solid rgba(255, 255, 255, 0.07);
  padding-top: 3px;
  width: 46px;
}
/* The palette's buttons match the dock's so the column reads as one run. */
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-btn {
  background: transparent;
  border: 0;
  color: rgba(255, 255, 255, 0.62);
}
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-btn:hover {
  background: rgba(255, 255, 255, 0.09);
  color: #fff;
}
/* Light grounds: the seam and the ink invert with everything else. */
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .wf-quickadd-row,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .wf-quickadd-row {
  border-top-color: rgba(12, 14, 18, 0.12);
}
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-btn,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-btn {
  background: transparent;
  border: 0;
  color: rgba(12, 14, 18, 0.7);
}
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-btn:hover,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-btn:hover {
  background: rgba(12, 14, 18, 0.09);
  color: #101216;
}
/* Measured 2px of daylight still showing at the join: the dock grew by its
   3px bottom padding after the cap was removed. Pulled flush. */
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-row {
  transform: translateY(39px);
}

/* No seam. A hairline between the two halves is still a divider, which is the
   thing being removed - the toolbar is one object. */
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-row,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .wf-quickadd-row,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .wf-quickadd-row {
  border-top: 0;
  padding-top: 4px;
}

/* The V and H hints were 8px type at a -3px offset, half outside the button
   and landing on fractional pixels, which is what made them look smeared.
   They sit fully inside the button on whole pixels, at a size that can
   actually be read, with a solid chip behind them. */
#view-workflow .wf-tool-btn { position: relative; overflow: visible; }
#view-workflow .wf-tool-key {
  right: 0;
  bottom: 0;
  min-width: 13px;
  height: 13px;
  padding: 0 2px;
  border-radius: 4px;
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  font-weight: 600;
  line-height: 13px;
  text-align: center;
  letter-spacing: 0;
  background: rgba(255, 255, 255, 0.16);
  border: 1px solid rgba(255, 255, 255, 0.10);
  color: rgba(255, 255, 255, 0.9);
  /* Whole-pixel placement: the badge is small enough that half a pixel of
     subpixel offset is visible as blur. */
  transform: translate(1px, 1px);
}
#view-workflow .wf-tool-btn.active .wf-tool-key {
  background: #FFFFFF;
  border-color: #FFFFFF;
  color: #17171A;
}
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .wf-tool-key,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .wf-tool-key {
  background: rgba(12, 14, 18, 0.12);
  border-color: rgba(12, 14, 18, 0.14);
  color: rgba(12, 14, 18, 0.82);
}

/* The port rings on section labels took each view's hue, so the same control
   surface read teal on Image, blue on Characters, amber on Storyboard and
   pink on Avatar - four systems instead of one. They are black everywhere:
   black IS the accent here, and on a light control panel it is the crispest
   mark available. Per-view colour stays where it identifies something (the
   node hues on the canvas), not on a label that means the same thing on
   every page. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(.section-label, .field > label)::before {
  border-color: var(--accent);
}

/* The little square corners on the lower sections. The rounded card is the
   WRAPPER (.video-console-bottom, radius 14px); the .panel inside it carries
   radius 0 and the identical rgb(238,241,243) fill while sitting 2px proud on
   every side, so its square corners poked out past the wrapper's rounding.

   Two surfaces painting the same colour, one of them square. The inner panel
   stops being a surface: the wrapper is the card, and the corner artifact goes
   with it. Same reasoning as the node and the viewfinder - one object, one
   edge. */
#view-video .video-console-bottom > .panel,
#view-avatar .video-console-bottom > .panel,
.video-console-bottom > .panel {
  background: none;
  border: 0;
  box-shadow: none;
  border-radius: 0;
}
/* Belt and braces: the wrapper clips to its own radius, so nothing inside can
   reintroduce a corner. */
.video-console-bottom { overflow: hidden; }

/* Same artifact, five other views: a square .thumb sitting flush in the corner
   of a rounded .entity-card, so the image's corner pokes past the card's. The
   thumbnail is real content rather than a redundant surface, so the card clips
   it instead of the fill being removed. */
.entity-card { overflow: hidden; }
.entity-card > .thumb,
.entity-card .thumb { border-radius: inherit; }

/* ═══════════════════════════════════════════════════════════════════════════
   GENERATION PAGES RETURN TO SILVER
   ───────────────────────────────────────────────────────────────────────────
   The dark shell was borrowed from a reference; brushed silver is this app's
   own identity. The nine pages where generation happens go back to it, with
   the dot grid inverted to black to suit. Workflow keeps its tone system:
   a canvas is judged against a ground the user picks, which is a different
   job from a form.

   Two consequences handled rather than discovered later. The panels are the
   same silver family as the ground, so they can no longer separate by value -
   they separate by elevation, which is what a plate on a chassis should do
   anyway. And every piece of ink that was flipped light for the dark shell has
   to flip back, or the page ships with invisible headings.
   ═══════════════════════════════════════════════════════════════════════════ */

/* SUPERSEDED by the real material below. This was a vertical grey gradient
   with a 5%-alpha stripe: a dull gradient, not brushed metal. Metal needs
   visible directional grain, and no stack of CSS gradients was going to
   produce it. */

/* Ink follows the ground: page-level type is light on #060608. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .panel-head h1 {
  color: #F2F3F5 !important;
}
/* Split by where the element actually sits. The original list treated these
   as one group because on the silver ground they shared a background; on the
   black ground they do not. .section-hint and .seg-btn appear BOTH directly
   on the page and inside the light control panels, so inking the whole class
   light turned the panel copies white-on-white - measured 1.01 to 1.23:1
   across seven views. Only the on-ground copies flip. */

/* Directly on the ground -> light ink. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .panel-head .panel-sub,
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .scene-group-title,
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .scene-subgroup-title,
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .gallery-empty,
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .scene-outputs-count {
  color: rgba(255, 255, 255, 0.66) !important;
}

/* Inside a light panel -> the chassis grades were always right here. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) :is(.panel, .control-panel, .card, .style-card, .console-grid,
             .video-console-grid, .scene-panel, .seg)
  :is(.section-hint, .seg-btn, .media-type-label, strong) {
  color: rgba(56, 61, 66, 0.92) !important;
}
/* The same classes when they sit over the ground instead of in a panel. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) > :is(.section-hint, .media-type-label),
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) > * > :is(.section-hint, .media-type-label) {
  color: rgba(255, 255, 255, 0.66) !important;
}

/* A plate on a chassis separates by lift, not by being a different silver. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .panel,
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .scene-section {
  background:
    linear-gradient(168deg, rgba(255,255,255,0.96), rgba(255,255,255,0.62) 46%),
    #F4F6F8;
  border: 1px solid rgba(255, 255, 255, 0.9);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.95),
    0 0 0 1px rgba(12, 18, 28, 0.07),
    0 18px 40px -20px rgba(12, 18, 28, 0.30),
    0 3px 10px -4px rgba(12, 18, 28, 0.14);
}

/* Back and Save stay white pills; they read on either ground. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .mobile-back-btn {
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(12, 18, 28, 0.14);
  color: #101216;
}

/* The viewfinder must be genuinely dark again. It was rgba(0,0,0,0.34), which
   was near-black over a near-black shell but is a mid grey over silver - so
   its light text measured 1.00 to 1.29 against it. On a silver page the
   aperture goes back to opaque --vf-bg, which is the whole point of it: a
   neutral dark ground to judge generated media against. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .viewfinder-panel {
  background: linear-gradient(180deg, var(--vf-surface-2), var(--vf-bg));
  border: 1px solid rgba(10, 10, 10, 0.55);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.7),
    inset 0 1px 3px rgba(0, 0, 0, 0.65),
    0 18px 44px -20px rgba(10, 14, 20, 0.55);
}

/* Headings inside a panel on a silver page take chassis ink, not shell ink. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(.panel, .scene-section) :is(h3, h4) {
  color: var(--text);
}
/* The Character Sheet pill is a dark chip, so its own ink stays light. */
:is(#view-characters, #view-charsheet, #view-scene, #view-storyboard, #view-shot-extractor)
  :is(.charsheet-pill-link, .charsheet-pill-arrow) {
  color: rgba(255, 255, 255, 0.92) !important;
}
:is(#view-remaster) .panel-head .panel-sub { color: rgba(16, 18, 22, 0.68) !important; }
:is(#view-scene) .scene-outputs-count {
  color: rgba(255, 255, 255, 0.55) !important;
}

/* Two contrast faults the audit found on the silver pages.

   The viewfinder panel set only a background-IMAGE, so its background-color
   stayed rgba(0,0,0,0.34) - a mid grey over silver rather than the near-black
   the light text needs. An explicit colour so the render and the measurement
   agree. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .viewfinder-panel {
  background-color: var(--vf-bg);
}
/* Section headings that sit directly on the page (History, and friends) still
   carried the dark shell's light ink. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(h3, h4, .history-block h3, .gallery-empty) {
  color: #F2F3F5;
}

/* Final four on the silver pages, all near-misses rather than invisible, but
   each still under 3:1.

   The empty-state glyph sits inside the dark aperture, so it takes the
   viewfinder's own muted grade rather than the page's. The Character Sheet
   pill is a dark chip on a light page, so its ink is white and its ground is
   pinned dark enough to earn it. The two helper-text cases take the chassis
   muted grade at full strength instead of a mixed one. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  .viewfinder-panel :is(.viewfinder-empty-glyph, .viewfinder-empty p) {
  color: var(--vf-text-muted) !important;
  opacity: 1;
}
.charsheet-pill-link {
  background: #14161A !important;
  border-color: rgba(255, 255, 255, 0.16) !important;
}
.charsheet-pill-link, .charsheet-pill-arrow { color: #FFFFFF !important; }
:is(#view-characters, #view-charsheet) .section-hint,
:is(#view-remaster) .panel-head .panel-sub {
  color: #3E4348 !important;
}
/* Settings kept the dark shell's light h1 on a dark page at 2.53:1. */
#view-settings h1, #view-settings .panel-head h1 { color: var(--shell-ink); }
/* The silver gradient runs #C3C3C3 down to #969696, so a muted #3E4348 is
   4.6:1 at the light stops but only ~2.5:1 across the mid band. Page-level
   subtitles take full-strength ink instead, which clears 5:1 at every stop
   rather than depending on where the reader happens to be scrolled. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  .panel-head .panel-sub {
  color: rgba(255, 255, 255, 0.66) !important;
  opacity: 1;
}

/* ==============================================================================
   ONE GROUND: THE WORKFLOW BLACK, ON EVERY GENERATION PAGE
   ----------------------------------------------------------------------------
   Exactly the workflow canvas: #060608 with the same 22px white dot grid at
   7.5% - not an approximation of it, the same two values the canvas reads.

   Painted on MAIN rather than on each view, so a page is one continuous
   surface instead of a panel sitting inside a page. The views contribute no
   background, no radius and no padding of their own.

   Everything that sat directly on the old silver was inked dark. On this
   ground that ink is invisible, so the page-level type is re-inked below;
   the panels and the viewfinder are unaffected - they carry their own
   surfaces. */
main:has(#view-image.active),
main:has(#view-characters.active),
main:has(#view-charsheet.active),
main:has(#view-scene.active),
main:has(#view-storyboard.active),
main:has(#view-shot-extractor.active),
main:has(#view-remaster.active),
main:has(#view-video.active),
main:has(#view-avatar.active) {
  background:
    radial-gradient(var(--app-dot, rgba(255,255,255,0.075)) 1.3px, transparent 1.3px) 0 0 / 22px 22px,
    var(--app-ground, #060608) !important;
  background-attachment: fixed, fixed !important;
}
/* The views themselves add nothing: the page IS the ground. */
#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
#view-shot-extractor, #view-remaster, #view-video, #view-avatar {
  background: none;
  border-radius: 0;
  padding: 0;
}


/* ==============================================================================
   THE RAIL AS A FLOATING CARD
   ------------------------------------------------------------------------------
   Taken from the supplied reference. What is copied is the TREATMENT, not the
   reference's content or its palette:

     - the rail is a detached rounded card with a soft drop shadow, rather than
       a full-height column welded to the viewport edge by a border
     - identity leads: avatar, an uppercase role eyebrow, then the name, sitting
       ABOVE the navigation instead of buried at the foot of it
     - navigation items are clean filled pills with no leading marker bar
     - one standing primary action pinned to the foot, full width when the rail
       is open and a circle when it is collapsed
     - collapsed is the same card, narrower - not a different object

   What is NOT copied, because ShotBorn has no equivalent and inventing one
   would be adding features rather than restyling: the reference's nested
   sub-navigation with its connector line and collapsed flyout, its Messages
   list, and its promotional card. ShotBorn's nav is flat.

   The reference's accent is orange. ShotBorn's accent is black, which is the
   loudest possible mark on the silver chassis but disappears on this near-black
   rail - so the primary action takes the app's own inversion of that rule,
   white on near-black, the same treatment the workflow node's Generate button
   already uses. No hue is introduced. */

.rail {
  margin: 14px 0 14px 14px;
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-right: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: 22px;
  box-shadow:
    0 26px 64px -22px rgba(0, 0, 0, 0.78),
    0 8px 20px -10px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 255, 255, 0.07);
  padding: 18px 14px;
  gap: 18px;
}
/* The card has 28px of vertical margin, so a full 100vh would overflow it. */
@media (min-width: 861px) {
  .rail { height: calc(100vh - 28px); }
}

/* ---------- identity block, now leading ---------- */
.rail-account {
  order: -1;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.03);
  padding: 11px 12px;
}
.rail-account:hover { background: rgba(255, 255, 255, 0.06); }
.rail-account-top { gap: 11px; }
.account-avatar {
  width: 36px; height: 36px;
  border-radius: 999px;
  flex: none;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
/* Eyebrow above name, the way the reference orders it: the role is the quiet
   qualifier and the name is the thing you read. */
.account-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.account-meta .account-sub {
  order: -1;
  font-family: var(--font-mono);
  font-size: 0.53125rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.42);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.account-meta .account-name {
  font-size: 0.84375rem;
  font-weight: 600;
  color: #F2F3F5;
  letter-spacing: -0.005em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ---------- navigation ---------- */
.rail-nav { gap: 2px; }
.rail-btn {
  border-left: 0;
  border-radius: 11px;
  padding: 9px 12px;
}
/* The reference's active state is a plain filled pill. The marker bar goes:
   a filled pill and a bar are two ways of saying the same thing. */
.rail-btn.active {
  background: rgba(255, 255, 255, 0.10);
  border-left-color: transparent;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
}
.rail-btn.active .rail-btn-icon { filter: none; }
.rail-group-label { padding: 15px 12px 5px; }

/* ---------- standing primary action ---------- */
.rail-cta {
  margin-top: auto;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 44px;
  padding: 0 14px;
  border: 0;
  border-radius: 12px;
  background: #F2F3F5;
  color: #0A0A0C;
  font-family: var(--font-body);
  font-size: 0.84375rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 8px 20px -10px rgba(0, 0, 0, 0.8);
  transition: background 0.18s ease, transform 0.12s ease;
}
.rail-cta:hover { background: #FFFFFF; }
.rail-cta:active { transform: translateY(1px); }
.rail-cta:focus-visible { outline: 2px solid #FFFFFF; outline-offset: 2px; }
.rail-cta-icon { font-size: 1rem; line-height: 1; font-weight: 500; }

/* ---------- collapsed: the same card, narrower ---------- */
#app-shell.rail-collapsed { grid-template-columns: 88px minmax(0, 1fr); }
#app-shell.rail-collapsed .rail { padding-left: 12px; padding-right: 12px; }
#app-shell.rail-collapsed .rail-account {
  padding: 9px 6px;
  justify-content: center;
}
#app-shell.rail-collapsed .rail-cta {
  width: 46px; height: 46px;
  align-self: center;
  padding: 0;
  border-radius: 999px;
}
#app-shell.rail-collapsed .rail-cta-label { display: none; }

/* The rail itself was the scroll container, so `margin-top:auto` on the action
   put it at the end of the SCROLL CONTENT rather than at the foot of the card -
   measured at y=1448 on a 940px viewport, i.e. permanently off screen. In the
   reference the action is always visible.

   So the scrolling moves inward one level: the card no longer scrolls, the
   panel fills it as a flex column, and only the nav list scrolls. Identity
   stays pinned to the top of the card and the action to its foot, with the
   list moving between them. min-height:0 is what actually allows the nav to
   shrink below its content height inside a flex column. */
@media (min-width: 861px) {
  .rail { overflow: hidden; }
  .rail-panel {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    gap: 14px;
  }
  .rail-nav {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    /* The list fades under the pinned action rather than being cut by a hard
       edge, so it reads as continuing rather than ending. */
    mask-image: linear-gradient(to bottom, #000 0, #000 calc(100% - 18px), transparent 100%);
  }
  .rail-nav::-webkit-scrollbar { display: none; width: 0; }
  .rail-cta { margin-top: 0; }
}

/* The shell is already inset: #app-shell sits at y=20 and is itself 100vh
   tall, so it runs 20px past the bottom of the viewport. That was invisible
   while the rail's content simply scrolled, but a pinned action at the foot
   of the card lands in exactly that clipped strip. Measured: card bottom 946
   against a 940 viewport, so the button was cut.

   The card is sized to the VISIBLE area rather than to the shell: 20px for
   the shell's own top offset plus the card's own 14px margins. The shell
   offset itself is left alone - it predates this and re-seating it would
   move every page, not just the rail. */
@media (min-width: 861px) {
  .rail { height: calc(100vh - 48px); }
}

/* ==============================================================================
   THE RAIL IS PART OF THE PAGE AGAIN
   ------------------------------------------------------------------------------
   The pass above detached the rail into a floating rounded card, because that
   is what the reference showed. That was a misreading of the reference: it is
   a MOCKUP SHEET - four sidebar specimens laid on a backdrop so each can be
   seen whole. Its rounded corners and drop shadow belong to the presentation,
   not to the product. Lifting them into a real app turns the rail into a tab
   floating on top of the page, which is exactly what it looked like.

   What the reference is actually worth copying survives untouched: identity
   leading at the top, pill navigation, one pinned primary action, and a
   collapsed state that is the same object narrower. Only the card goes. */
.rail {
  margin: 0;
  border-radius: 0;
  border: 0;
  border-right: 1px solid rgba(255, 255, 255, 0.07);
  box-shadow: none;
}
@media (min-width: 861px) {
  /* Back to the full column height now that there are no margins to subtract. */
  .rail { height: 100vh; }
}
/* No scroll gutter anywhere in the rail: a visible scrollbar down the seam is
   the other half of what made the two areas read as separate documents. */
.rail, .rail-panel, .rail-nav {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.rail::-webkit-scrollbar,
.rail-panel::-webkit-scrollbar,
.rail-nav::-webkit-scrollbar { display: none; width: 0; height: 0; }

/* ==============================================================================
   ONE STEP DOWN IN SCALE
   ------------------------------------------------------------------------------
   Measured at 1920x1080 before changing anything: h1 32px, body copy 14.5px,
   form controls 43px tall, the primary action 52px, nav rows 39px, the rail
   300px wide. On a large display that reads as a zoomed-in interface rather
   than a dense one.

   Everything below is the same design one step smaller - roughly 10-12% off
   type and control heights, and the same off the rail and the page gutters so
   the proportions hold. Nothing is restyled; only the scale changes. Touch
   targets stay above 44px where they are real touch targets (the primary
   action lands at 44, the nav rows are pointer targets on desktop only). */

/* ---------- shell ---------- */
.app { grid-template-columns: 264px minmax(0, 1fr); }
#app-shell.rail-collapsed { grid-template-columns: 78px minmax(0, 1fr); }
main { padding: 30px 36px 52px; max-width: 1120px; }

/* The shell sits 20px down and is itself 100vh, so a full-height rail runs
   past the bottom and clips the pinned action by exactly that much. */
@media (min-width: 861px) {
  .rail { height: calc(100vh - 20px); }
}

/* ---------- rail ---------- */
.rail { padding: 15px 12px; }
.rail-btn { padding: 7px 11px; font-size: 0.8125rem; border-radius: 10px; gap: 10px; }
.rail-btn-label { font-size: 0.8125rem; }
.rail-btn-icon { font-size: 0.8125rem; }
.rail-group-label { font-size: 0.5625rem; padding: 13px 11px 4px; }
.rail-account { padding: 9px 10px; border-radius: 12px; }
.account-avatar { width: 32px; height: 32px; }
.account-meta .account-name { font-size: 0.78125rem; }
.account-meta .account-sub { font-size: 0.5rem; }
.rail-cta { height: 40px; font-size: 0.78125rem; border-radius: 11px; }
#app-shell.rail-collapsed .rail-cta { width: 40px; height: 40px; }

/* ---------- page type ---------- */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) {
  --g4: 15px; --g5: 22px; --g6: 30px;
}
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar) h1,
.panel-head h1 { font-size: 1.6875rem; }
.panel-head .panel-sub { font-size: 0.8125rem; }
.section-label, .field > label { font-size: 0.59375rem; }

/* ---------- controls ---------- */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(input[type="text"], input[type="number"], input[type="search"], select, textarea) {
  font-size: 0.8125rem;
  padding: 9px 11px;
}
.primary-btn { font-size: 0.84375rem; padding: 11px 20px; }
.secondary-btn, .lib-pick-btn { font-size: 0.78125rem; padding: 8px 14px; }
.panel, .control-panel { padding: 17px; }

/* ==============================================================================
   RAIL, SECOND REFERENCE
   ------------------------------------------------------------------------------
   What this reference does differently from the first, and what is taken:

     - identity sits at the FOOT, in its own band below a divider, not at the top
     - the active row is not a filled pill: the icon and label simply go to full
       strength while everything around them stays muted
     - a divider separates the last group from the list above it
     - collapsed is where the fill appears - a rounded tile behind the active
       icon, because with no label there is nothing else to carry the state

   Per instruction: no sections are invented (the standing action from the
   previous pass is removed - ShotBorn has no such item), the existing group
   headers are kept even though this reference has none, and the palette stays
   ShotBorn's. The reference's blue is not imported; full-strength ink on the
   dark rail is this app's equivalent of it. */

/* identity returns to the foot */
.rail-account {
  order: 0;
  margin-top: auto;
  border: 0;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 0;
  background: transparent;
  padding: 13px 11px 4px;
}
.rail-account:hover { background: transparent; }
.rail-account:hover .account-name { color: #FFFFFF; }
/* name first, qualifier under it - this reference's order, and the markup's */
.account-meta .account-sub {
  order: 0;
  font-family: var(--font-body);
  font-size: 0.6875rem;
  letter-spacing: 0;
  text-transform: none;
  color: rgba(255, 255, 255, 0.42);
}
.account-meta .account-name { font-size: 0.78125rem; font-weight: 600; }

/* the active row carries state in ink, not in a fill */
.rail-btn.active {
  background: transparent;
  box-shadow: none;
  color: #FFFFFF;
  font-weight: 600;
}
.rail-btn.active .rail-btn-icon { color: #FFFFFF; }
.rail-btn:hover:not(.active) { background: rgba(255, 255, 255, 0.05); }

/* a divider above the last group, the way the reference separates its footer */
.rail-nav .rail-group-label:last-of-type,
#rail-group-label-system {
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  margin-top: 10px;
  padding-top: 14px;
}

/* collapsed: with the label gone, the fill is what carries the active state */
#app-shell.rail-collapsed .rail-btn.active {
  background: rgba(255, 255, 255, 0.12);
  border-radius: 10px;
}
#app-shell.rail-collapsed .rail-account {
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  padding: 12px 0 4px;
}

/* The standing New Project action, kept by request. It sits between the nav
   and the identity band: the list scrolls, this does not, and the identity
   stays the last thing in the rail the way the reference has it.

   `flex: 1` on the nav already claims the leftover space, so neither the
   action nor the identity needs an auto margin to be pushed down - and an
   auto margin on both would fight for the same gap. */
.rail-cta { margin-top: 0; margin-bottom: 2px; }
.rail-account { margin-top: 0; }
@media (min-width: 861px) { .rail-cta { margin-top: 0; } }

/* ==============================================================================
   APP BAR + SLIM RAIL  (the A+B hybrid)
   ------------------------------------------------------------------------------
   The shell becomes a full-width top row over a rail+main row. The rail holds
   only Home/Projects and the ten generation tools now, so it never scrolls;
   global chrome and the Library/Explore sections live in the app bar.

   Appended last on purpose: several earlier passes set `.rail { height: ... }`
   for the old full-height column, and equal-specificity source order is what
   lets these win without !important. */

.app {
  display: grid;
  grid-template-columns: 240px minmax(0, 1fr);
  grid-template-rows: 52px minmax(0, 1fr);
  grid-template-areas:
    "top  top"
    "rail main";
  min-height: 100vh;
}
.appbar { grid-area: top; }
.rail   { grid-area: rail; }
main    { grid-area: main; }

/* ---------- the bar ---------- */
.appbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 16px;
  background: #121212;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  z-index: 20;
}
.appbar-left { display: flex; align-items: center; gap: 10px; flex: none; }
.appbar-logo { height: 26px; width: auto; display: block; }
.appbar-nav { display: flex; align-items: center; gap: 4px; }
.appbar-right { display: flex; align-items: center; gap: 6px; margin-left: auto; }

/* section links + the Library trigger */
.appbar-link,
.appbar-menu-trigger {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 34px;
  padding: 0 12px;
  background: transparent;
  border: 0;
  border-radius: 9px;
  color: rgba(255, 255, 255, 0.72);
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s ease, color 0.15s ease;
}
.appbar .rail-btn-icon { color: inherit; font-size: 0.8125rem; }
.appbar-link:hover,
.appbar-menu-trigger:hover { background: rgba(255, 255, 255, 0.06); color: #fff; }
/* active section (its data-view page is open) reads in full-strength ink */
.appbar-link.active,
.appbar-menu:has(.rail-btn.active) .appbar-menu-trigger {
  background: rgba(255, 255, 255, 0.10);
  color: #fff;
}
.appbar-caret { font-size: 0.5625rem; opacity: 0.6; margin-left: 1px; }

/* ---------- Library dropdown ---------- */
.appbar-menu { position: relative; }
.appbar-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 200px;
  background: #1a1a1c;
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 12px;
  box-shadow: 0 18px 44px -18px rgba(0, 0, 0, 0.85);
  padding: 6px;
  display: none;
  flex-direction: column;
  gap: 1px;
  z-index: 40;
}
.appbar-menu.open .appbar-dropdown { display: flex; }
.appbar-dropdown .rail-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  border: 0;
  border-left: 0;
  border-radius: 8px;
  padding: 8px 10px;
  background: transparent;
  color: rgba(255, 255, 255, 0.82);
  font-size: 0.8125rem;
  cursor: pointer;
}
.appbar-dropdown .rail-btn:hover { background: rgba(255, 255, 255, 0.07); color: #fff; }
.appbar-dropdown .rail-btn.active { background: rgba(255, 255, 255, 0.12); color: #fff; }
.appbar-dropdown .rail-btn-icon { color: inherit; }

/* ---------- right cluster ---------- */
.appbar-agent {
  display: inline-flex; align-items: center; gap: 7px;
  height: 34px; padding: 0 12px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 9px;
  color: rgba(255, 255, 255, 0.85);
  font-family: var(--font-body); font-size: 0.8125rem; font-weight: 500;
  cursor: pointer; white-space: nowrap;
  transition: background 0.15s ease, color 0.15s ease;
}
.appbar-agent:hover { background: rgba(255, 255, 255, 0.10); color: #fff; }
.appbar-agent.active { background: rgba(255,255,255,0.14); color: #fff; }

.appbar-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  background: transparent; border: 0; border-left: 0; border-radius: 9px;
  color: rgba(255, 255, 255, 0.62); cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.appbar-icon-btn .rail-btn-icon { color: inherit; font-size: 0.9375rem; }
.appbar-icon-btn:hover { background: rgba(255, 255, 255, 0.06); color: #fff; }
.appbar-icon-btn.active { background: rgba(255, 255, 255, 0.10); color: #fff; }

.appbar-account {
  display: inline-flex; align-items: center; gap: 9px;
  height: 38px; padding: 0 10px 0 6px;
  background: transparent; border: 0; border-radius: 10px;
  cursor: pointer; max-width: 190px;
  transition: background 0.15s ease;
}
.appbar-account:hover { background: rgba(255, 255, 255, 0.06); }
.appbar-account .account-avatar {
  width: 28px; height: 28px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.appbar-account .account-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; text-align: left; }
.appbar-account .account-name { font-size: 0.75rem; font-weight: 600; color: #f2f3f5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.appbar-account .account-sub { font-size: 0.625rem; color: rgba(255, 255, 255, 0.42); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Live credit balance shown under the name in the top bar (restored after the
   profile moved off the sidebar). It lives inside .account-meta so it inherits
   the meta's narrow-layout hiding, and drops the sidebar version's
   border-top/padding-top divider. It replaces the role sub-line here — the role
   still shows on the Profile page. Guests/local mode keep [hidden] -> no row. */
.appbar-account .account-sub { display: none; }
.appbar-account .rail-account-credits {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 0; border: 0; margin: 0;
  font-family: var(--font-mono); font-size: 0.65625rem; font-weight: 700;
  color: rgba(255, 255, 255, 0.66);
}
.appbar-account .rail-account-credits[hidden] { display: none; }
.appbar-account .rail-account-credits-icon { font-size: 0.625rem; color: rgba(255, 255, 255, 0.5); }
.appbar-account .rail-account-credits-unit { color: rgba(255, 255, 255, 0.4); }

.appbar-cta {
  display: inline-flex; align-items: center; gap: 6px;
  height: 36px; padding: 0 14px;
  background: #f2f3f5; color: #0a0a0c;
  border: 0; border-radius: 10px;
  font-family: var(--font-body); font-size: 0.8125rem; font-weight: 600;
  cursor: pointer; white-space: nowrap; flex: none;
  transition: background 0.15s ease, transform 0.1s ease;
}
.appbar-cta:hover { background: #fff; }
.appbar-cta:active { transform: translateY(1px); }
.appbar-cta-icon { font-size: 1rem; line-height: 1; }

/* ---------- the slim rail (nav only now) ---------- */
.rail {
  margin: 0;
  padding: 12px 12px;
  border: 0;
  border-right: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: 0;
  box-shadow: none;
  gap: 0;
  height: auto;
  min-height: 0;
}
.rail-panel { display: block; }
.rail-nav { gap: 2px; }

/* ---------- scroll architecture with the top row ---------- */
@media (min-width: 861px) {
  html, body { height: 100%; overflow: hidden; }
  /* dvh, not vh — see the note on the earlier min-width:861 block. This later
     rule wins by source order, so the dynamic-viewport height has to be set
     here too or iPad-landscape Safari keeps overshooting to the tall 100vh. */
  .app { height: 100vh; height: 100dvh; overflow: hidden; }
  .rail { height: auto; min-height: 0; overflow-y: auto; overflow-x: hidden; }
  main  { height: auto; min-height: 0; overflow-y: auto; }
  .rail, .rail-panel, .rail-nav { scrollbar-width: none; -ms-overflow-style: none; }
  .rail::-webkit-scrollbar, .rail-nav::-webkit-scrollbar { display: none; width: 0; height: 0; }
}

/* ---------- landing (pre-ENTER home): full-bleed, no chrome ---------- */
#app-shell.landing-active {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  grid-template-areas: "main";
}
#app-shell.landing-active .appbar,
#app-shell.landing-active .rail { display: none; }

/* ---------- collapsed rail ---------- */
#app-shell.rail-collapsed { grid-template-columns: 74px minmax(0, 1fr); }
#app-shell.rail-collapsed .rail-btn-label { display: none; }
#app-shell.rail-collapsed .rail-group-label { text-align: center; font-size: 0.5rem; }
#app-shell.rail-collapsed .rail-btn { justify-content: center; padding: 8px; }
#app-shell.rail-collapsed .rail-collapse-btn { transform: rotate(180deg); }

/* ---------- mobile: bar spans top, rail becomes a drawer ---------- */
@media (max-width: 860px) {
  .app { grid-template-columns: minmax(0, 1fr); grid-template-rows: 52px minmax(0,1fr); grid-template-areas: "top" "main"; }
  .appbar-nav { display: none; }
  .appbar-account .account-meta { display: none; }
  .rail {
    position: fixed; top: 52px; left: 0; bottom: 0; width: 260px;
    background: #121212; z-index: 50;
    transform: translateX(-100%); transition: transform 0.2s ease;
  }
  .rail.open { transform: translateX(0); }
  .rail-collapse-btn { display: none; }
}
@media (min-width: 861px) {
  .rail-toggle { display: none; }
}

/* ==============================================================================
   PLUMAR-STYLE ICON RAIL: collapsed to icons, reveals labels on hover, pinnable
   ------------------------------------------------------------------------------
   The rail reserves a 64px icon strip. Its inner panel is absolutely positioned
   over that strip and, on hover, widens to 232px OVER the workspace without
   reflowing it. Pinning widens the grid column instead, so the expanded rail
   sits beside the content permanently. Appended last so it wins the earlier
   .rail / grid rules by source order. */

.app {
  grid-template-columns: 64px minmax(0, 1fr);
  grid-template-rows: 52px minmax(0, 1fr);
  grid-template-areas: "top top" "rail main";
}

.rail {
  grid-area: rail;
  position: relative;
  width: 64px;
  padding: 0;
  margin: 0;
  border: 0;
  background: transparent;
  overflow: visible;
  height: auto;
}

.rail-panel {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: 64px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 10px;
  background: #121212;
  border-right: 1px solid rgba(255, 255, 255, 0.07);
  overflow: hidden;
  z-index: 30;
  transition: width 0.18s ease, box-shadow 0.18s ease;
}
.rail:hover .rail-panel,
#app-shell.rail-pinned .rail-panel {
  width: 232px;
  box-shadow: 12px 0 34px -14px rgba(0, 0, 0, 0.7);
}
#app-shell.rail-pinned { grid-template-columns: 232px minmax(0, 1fr); }
#app-shell.rail-pinned .rail-panel { box-shadow: none; }

.rail-nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow-y: auto;
  overflow-x: hidden;
  min-height: 0;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.rail-nav::-webkit-scrollbar { display: none; width: 0; }

/* buttons: icon centred while collapsed, icon+label left-aligned when open */
.rail-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  height: 40px;
  padding: 0 10px;
  border: 0;
  border-left: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--vf-text-muted);
  cursor: pointer;
  white-space: nowrap;
}
.rail-btn-icon { width: 24px; flex: none; display: inline-flex; align-items: center; justify-content: center; font-size: 0.9375rem; }
.rail-btn-label { display: none; }
.rail-btn:hover { background: rgba(255, 255, 255, 0.06); color: var(--vf-text); }
.rail-btn.active { background: rgba(255, 255, 255, 0.10); color: var(--vf-text); }
.rail-btn.active .rail-btn-icon { color: var(--vf-text); }

.rail-group-label { display: none; padding: 12px 12px 4px; font-size: 0.5625rem; letter-spacing: 0.12em; }

/* pin control */
.rail-pin-btn {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  height: 34px; margin-bottom: 6px; padding: 0 10px;
  background: transparent; border: 0; border-radius: 10px;
  color: var(--vf-text-faint); cursor: pointer;
}
.rail-pin-btn .rail-pin-icon { width: 24px; flex: none; text-align: center; font-size: 0.8125rem; }
.rail-pin-btn .rail-btn-label { display: none; }
.rail-pin-btn:hover { background: rgba(255, 255, 255, 0.06); color: var(--vf-text); }
#app-shell.rail-pinned .rail-pin-btn { color: var(--vf-text); }

/* revealed state: labels + group labels appear, everything left-aligns */
.rail:hover .rail-btn,
.rail:hover .rail-pin-btn,
#app-shell.rail-pinned .rail-btn,
#app-shell.rail-pinned .rail-pin-btn { justify-content: flex-start; }
.rail:hover .rail-btn-label,
.rail:hover .rail-pin-btn .rail-btn-label,
#app-shell.rail-pinned .rail-btn-label,
#app-shell.rail-pinned .rail-pin-btn .rail-btn-label { display: inline; }
.rail:hover .rail-group-label,
#app-shell.rail-pinned .rail-group-label { display: block; }
/* the Edit group label stays hidden until its feature flag clears .hidden */
.rail-group-label.hidden { display: none !important; }
.rail-btn.hidden { display: none !important; }

/* ---------- scroll architecture ---------- */
@media (min-width: 861px) {
  html, body { height: 100%; overflow: hidden; }
  .app { height: 100vh; overflow: hidden; }
  main { height: auto; min-height: 0; overflow-y: auto; }
  .rail-toggle { display: none; }
}

/* ---------- landing: no chrome ---------- */
#app-shell.landing-active {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  grid-template-areas: "main";
}
#app-shell.landing-active .appbar,
#app-shell.landing-active .rail { display: none; }

/* ---------- mobile: full labelled drawer ---------- */
@media (max-width: 860px) {
  .app { grid-template-columns: minmax(0, 1fr); grid-template-rows: 52px minmax(0, 1fr); grid-template-areas: "top" "main"; }
  .appbar-account .account-meta { display: none; }
  .rail { position: static; width: 0; }
  .rail-panel {
    position: fixed; top: 52px; left: 0; bottom: 0; width: 260px;
    transform: translateX(-100%); transition: transform 0.2s ease; z-index: 50;
  }
  .rail.open .rail-panel { transform: translateX(0); }
  .rail.open .rail-btn { justify-content: flex-start; }
  .rail.open .rail-btn-label { display: inline; }
  .rail.open .rail-group-label { display: block; }
  .rail-pin-btn { display: none; }
}

/* Rail icons follow the app's dark-chrome ink tokens explicitly (an older
   .rail-btn-icon rule otherwise wins by selector specificity over the
   inherited colour). */
.rail-nav .rail-btn .rail-btn-icon { color: var(--vf-text-muted); }
.rail-nav .rail-btn:hover .rail-btn-icon,
.rail-nav .rail-btn.active .rail-btn-icon { color: var(--vf-text); }

/* ==============================================================================
   PLUMAR, FAITHFULLY: floating transparent chrome over a full-bleed workspace
   ------------------------------------------------------------------------------
   The earlier pass kept a solid rail panel and a full-width bar. Plumar has
   neither: its icons float directly on the canvas with no panel, and its top
   controls are floating rounded pills, not an edge-to-edge bar. This makes the
   chrome float.

   Layout inverts: the shell stops reserving columns for chrome. main fills the
   whole shell; the app bar and the rail are fixed overlays on top of it, and
   main is padded so its content clears them. Appended last to win. */

.app {
  display: block;
  height: auto;
  min-height: 100vh;
}

/* ---------- app bar: a transparent strip holding two floating pills ---------- */
.appbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  background: transparent;
  border: 0;
  z-index: 40;
  pointer-events: none;
}
.appbar > * { pointer-events: auto; }
.appbar-left,
.appbar-right {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 44px;
  padding: 0 8px 0 12px;
  background: rgba(20, 20, 22, 0.82);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.6);
}
.appbar-logo { height: 24px; width: auto; display: block; }

/* ---------- rail: floating icons, no panel ---------- */
.rail {
  position: fixed;
  left: 12px;
  top: 72px;
  bottom: 12px;
  width: auto;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  overflow: visible;
  z-index: 35;
}
.rail-panel {
  position: static;
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 56px;
  height: 100%;
  padding: 8px 8px;
  background: transparent;
  border: 0;
  border-radius: 18px;
  box-shadow: none;
  overflow: visible;
  transition: width 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
}
/* On hover the whole column widens and, only then, gets a soft floating
   backing so labels stay legible where they cross over page content. */
.rail:hover .rail-panel,
#app-shell.rail-pinned .rail-panel {
  width: 210px;
  background: rgba(18, 18, 20, 0.9);
  -webkit-backdrop-filter: blur(20px) saturate(150%);
  backdrop-filter: blur(20px) saturate(150%);
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 20px 50px -18px rgba(0, 0, 0, 0.7);
}

.rail-nav {
  display: flex; flex-direction: column; gap: 4px;
  overflow-y: auto; overflow-x: hidden; min-height: 0;
  scrollbar-width: none; -ms-overflow-style: none;
}
.rail-nav::-webkit-scrollbar { display: none; width: 0; }

/* no group labels in this style */
.rail-group-label { display: none !important; }

.rail-btn {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  height: 40px; padding: 0 8px;
  border: 0; border-left: 0; border-radius: 12px;
  background: transparent; color: var(--vf-text-muted);
  cursor: pointer; white-space: nowrap;
}
.rail-btn-icon { width: 24px; flex: none; display: inline-flex; align-items: center; justify-content: center; font-size: 1rem; }
.rail-btn-label { display: none; }
.rail-btn:hover { background: rgba(255, 255, 255, 0.07); color: var(--vf-text); }
.rail-btn.active { background: rgba(255, 255, 255, 0.12); color: var(--vf-text); }

.rail:hover .rail-btn,
#app-shell.rail-pinned .rail-btn { justify-content: flex-start; }
.rail:hover .rail-btn-label,
#app-shell.rail-pinned .rail-btn-label { display: inline; }
.rail-btn.hidden { display: none !important; }

/* pin becomes a quiet collapse chevron pinned to the foot of the rail */
.rail-pin-btn {
  order: 99; margin-top: auto; margin-bottom: 0;
  display: flex; align-items: center; justify-content: center; gap: 12px;
  height: 34px; padding: 0 8px;
  background: transparent; border: 0; border-radius: 12px;
  color: var(--vf-text-faint); cursor: pointer;
}
.rail-pin-btn .rail-pin-icon { width: 24px; flex: none; text-align: center; font-size: 0.75rem; }
.rail-pin-btn .rail-btn-label { display: none; }
.rail:hover .rail-pin-btn { justify-content: flex-start; }
.rail:hover .rail-pin-btn .rail-btn-label,
#app-shell.rail-pinned .rail-pin-btn .rail-btn-label { display: inline; }
.rail-pin-btn:hover { background: rgba(255, 255, 255, 0.07); color: var(--vf-text); }
#app-shell.rail-pinned .rail-pin-btn { color: var(--vf-text); }

/* ---------- main fills the shell, padded to clear the floating chrome ---------- */
main {
  padding-top: 72px;
  padding-left: 84px;
}
@media (min-width: 861px) {
  html, body { height: 100%; overflow: hidden; }
  .app { height: 100vh; overflow: hidden; }
  main { height: 100vh; overflow-y: auto; }
  .rail-toggle { display: none; }
}
/* when pinned, hold the rail open and give main room for the wider column */
/* pinned shift lives on the view, not main - see the rule below (removed the
   duplicate 236px main padding that doubled the offset). */

/* ---------- landing: nothing floats ---------- */
#app-shell.landing-active .appbar,
#app-shell.landing-active .rail { display: none; }
#app-shell.landing-active main { padding: 0; }

/* ---------- mobile ---------- */
@media (max-width: 860px) {
  .appbar-right .account-meta { display: none; }
  main { padding-left: 16px; padding-right: 16px; }
  .rail { left: 0; top: 60px; }
  .rail-panel {
    position: fixed; top: 60px; left: 0; bottom: 0; width: 240px;
    background: rgba(18,18,20,0.96); border-right: 1px solid rgba(255,255,255,0.08);
    transform: translateX(-100%); transition: transform 0.2s ease; z-index: 50;
  }
  .rail.open .rail-panel { transform: translateX(0); }
  .rail.open .rail-btn { justify-content: flex-start; }
  .rail.open .rail-btn-label { display: inline; }
  .rail-pin-btn { display: none; }
}

/* Chrome clearance. The floating bar and rail overlay the page, so page
   CONTENT must start clear of them - but the dark dot ground stays full-bleed
   (main keeps padding:0), so the offset goes on the view, not on main. The
   ground shows through the transparent inset, dots running under the floating
   icons exactly as the reference canvas does. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar).active {
  padding: 78px 30px 48px 96px;
}
/* Views that render through main's own padding just need a larger top/left
   minimum so nothing tucks under the floating chrome. */
main:has(> #view-projects.active),
main:has(> #view-explore.active) { padding: 78px 36px 52px 96px; }
main:has(> #view-workflow.active) { padding-top: 70px; }

/* General chrome clearance for every routed view. Many views carry their own
   #view-X.active padding that ignores main, so this outranks them all
   (#app-shell main > .view.active = higher specificity than #view-X.active)
   and guarantees a top/left safe area under the floating pills and icon rail.
   Home is the full-bleed landing (chrome hidden); workflow breaks out of main
   with negative margins and is handled on its own line above. */
#app-shell main > .view.active:not(#view-home):not(#view-workflow) {
  padding-top: 78px;
  padding-left: 96px;
}

/* Workflow keeps its full-bleed canvas (Plumar-like), so only its header row
   is nudged clear of the floating pills and icon rail - not the whole view. */
#view-workflow.active .panel-head.panel-head-row {
  margin-top: 46px;
  padding-left: 56px;
  padding-right: 6px;
}

/* ==============================================================================
   RAIL: TRANSPARENT EXPANDED MENU + PAGE SHIFTS RIGHT ON EXPAND
   ------------------------------------------------------------------------------
   Two corrections. (1) The expanded menu must be transparent - just the icons
   and labels, never the box they sit in - so the backing that the hover state
   added is removed. (2) Expanding must not overlap the page: the content shifts
   right by the menu's width so the revealed labels sit in vacated dark ground,
   which also keeps them legible without any backing. */

.rail:hover .rail-panel,
#app-shell.rail-pinned .rail-panel {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border: 0;
  box-shadow: none;
}

/* the shift. main keeps its full-bleed dark ground; only the view content
   slides right, so the dot ground still runs under the floating icons. */
main > .view { transition: padding-left 0.16s ease; }
/* SUPERSEDED: the per-view padding shift did not RE-CENTRE a margin:auto view;
   it just pushed content right inside the box. Recentring is now done by
   padding main itself on hover (below), so a centred view re-centres in the
   space left of the expanding rail. This rule is neutralised. */
#app-shell.rail-pinned main > .view.active:not(#view-home):not(#view-workflow) {
  /* pin removed; kept only so the selector parses */
}
/* workflow keeps its full-bleed canvas; only its header row shifts */
#view-workflow.active .panel-head.panel-head-row { transition: padding-left 0.16s ease; }
#app-shell:has(.rail:hover) #view-workflow.active .panel-head.panel-head-row,
#app-shell.rail-pinned #view-workflow.active .panel-head.panel-head-row {
  padding-left: 200px;
}

/* ==============================================================================
   PHOSPHOR ICONS (MIT) - transparent line icons, inherit ink, no background
   ------------------------------------------------------------------------------
   Replaces the Unicode glyphs in the nav with Phosphor's regular set. The SVGs
   ship fill="currentColor", so they take the rail's ink tokens and carry no
   fill or box of their own. */
.rail-btn-icon .ph { width: 20px; height: 20px; display: block; }
.appbar-icon-btn .ph { width: 19px; height: 19px; display: block; }
.rail-pin-icon .ph { width: 17px; height: 17px; display: block; }
.rail-btn-icon, .appbar-icon-btn .rail-btn-icon { color: inherit; }

/* ==============================================================================
   APP BAR: ONE CENTERED, SLEEKER CAPSULE
   ------------------------------------------------------------------------------
   The two edge pills merge into a single capsule centered at the top, the way
   the reference floats one cluster. The left (logo) and right (chrome) groups
   sit flush with a hairline seam between them and share one rounded outline,
   so they read as one pill. Slimmer height, tighter spacing, a compact action.*/
.appbar { justify-content: center; gap: 0; }
.appbar-left,
.appbar-right {
  height: 40px;
  background: rgba(20, 20, 22, 0.86);
  -webkit-backdrop-filter: blur(20px) saturate(150%);
  backdrop-filter: blur(20px) saturate(150%);
  border: 1px solid rgba(255, 255, 255, 0.09);
  box-shadow: 0 8px 26px -12px rgba(0, 0, 0, 0.55);
}
.appbar-left {
  border-radius: 999px 0 0 999px;
  border-right: 1px solid rgba(255, 255, 255, 0.07);
  padding: 0 12px 0 12px;
  gap: 8px;
}
.appbar-right {
  border-radius: 0 999px 999px 0;
  border-left: 0;
  padding: 0 6px 0 8px;
  gap: 4px;
}
.appbar-logo { height: 22px; }

/* sleeker chrome inside the capsule */
.appbar-icon-btn { width: 30px; height: 30px; }
.appbar-icon-btn .ph { width: 17px; height: 17px; }
.appbar-account {
  height: 32px;
  padding: 0 8px 0 5px;
  gap: 7px;
  max-width: 160px;
}
.appbar-account .account-avatar { width: 24px; height: 24px; }
.appbar-account .account-name { font-size: 0.71875rem; }
/* drop the secondary line for a cleaner, sleeker readout */
.appbar-account .account-sub { display: none; }
.appbar-cta {
  height: 30px;
  padding: 0 13px;
  font-size: 0.75rem;
  border-radius: 999px;
}
.appbar-cta-icon { font-size: 0.875rem; }

/* The right group had margin-left:auto (from the two-pill layout) which split
   the capsule apart under justify-content:center. Flush again. */
.appbar-right { margin-left: 0; }

/* ==============================================================================
   PREMIUM LIGHT PANELS ON THE DARK GROUND (not black)
   ------------------------------------------------------------------------------
   The panels read as glaring white rectangles because each has a pure-white 1px
   border (rgba(255,255,255,0.9)) and only a white glow - so the light panel
   meets the near-black ground at a hard, high-contrast edge with nothing to
   seat it. Kept light: the harsh white border is softened, a real diffuse
   shadow lifts the panel off the ground, the lit top hairline stays for the
   machined sheen, and the fill is toned a few points off pure white so the
   contrast is present but refined rather than shouting. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(.panel, .control-panel, .scene-section, .card, .console-panel, .video-console-panel):not(.viewfinder-panel):not(.viewfinder) {
  background:
    linear-gradient(168deg, #EEF1F4 0%, #E4E8EC 60%, #DDE2E6 100%) !important;
  border: 1px solid rgba(70, 82, 96, 0.14) !important;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    0 1px 2px rgba(8, 10, 14, 0.35),
    0 26px 60px -26px rgba(0, 0, 0, 0.78),
    0 8px 22px -12px rgba(0, 0, 0, 0.5) !important;
}

/* ==============================================================================
   WORKFLOW: ONE CONTINUOUS DOTTED GROUND + TOOL DOCK ICONS
   ------------------------------------------------------------------------------
   The header used to sit on main's own surface while the canvas painted its
   dots below - a visible seam where "another section began". Now the node
   canvas covers the WHOLE view (behind the header too), so its dots and tone
   run edge to edge and the header simply floats over them. */
main:has(> #view-workflow.active) { background: var(--app-ground, #060608) !important; }
main:has(> #view-workflow.active)::before { display: none !important; }
#view-workflow.active .node-canvas-wrap {
  position: absolute !important;
  inset: 0 !important;
  z-index: 0;
}
#view-workflow.active .panel-head-row {
  position: relative;
  z-index: 2;
}
/* Tool dock icons are Phosphor now, sized to match. */
.wf-tool-btn .wf-tool-icon .ph { width: 17px; height: 17px; display: block; }

/* The header now floats over the interactive canvas, so its transparent areas
   must not swallow canvas clicks (double-click to add a node) - only its
   actual controls stay interactive. */
#view-workflow.active .panel-head-row { pointer-events: none; }
#view-workflow.active .panel-head-row button,
#view-workflow.active .panel-head-row a,
#view-workflow.active .panel-head-row .workflow-save-row { pointer-events: auto; }

/* ==============================================================================
   BACK BUTTON: MOBILE ONLY  +  WORKFLOW CANVAS EDGE-TO-EDGE
   ------------------------------------------------------------------------------
   With the rail always present on desktop, the per-page back button is
   redundant there; it stays for mobile where the rail is a hidden drawer. */
@media (min-width: 861px) {
  .mobile-back-btn { display: none !important; }
}

/* The workflow canvas must fill the whole viewport so its tone/dots reach every
   edge (they were stopping short at main's padding, leaving a dark band when
   the tone was light). Zero main's padding and the view's margins for workflow
   so the canvas covers 0,0 to the corners; the header is re-offset on its own
   to clear the floating bar and icon rail. */
main:has(> #view-workflow.active) { padding: 0 !important; }
#view-workflow.active { margin: 0 !important; }
#view-workflow.active .panel-head.panel-head-row {
  margin-top: 60px;
  padding-left: 78px;
}

/* ==============================================================================
   WORKFLOW IS ONE FULL-BLEED PAGE  +  TOOL DOCK CLEARS THE RAIL  +  PREMIUM BAR
   ------------------------------------------------------------------------------
   1) The canvas carried border-radius:20px and an inset recess frame, so it
      read as a rounded box (the "curve") instead of one page. Squared off and
      unframed - the edge is the viewport, nothing encloses it. */
#view-workflow.active,
#view-workflow.active .node-canvas-wrap,
#view-workflow.active .node-canvas {
  border-radius: 0 !important;
}
#view-workflow.active .node-canvas-wrap { box-shadow: none !important; }

/* 2) The hand/select dock sat at left:16px, under the icon rail. Push it clear
      of the rail so it is never hidden beneath the menu. */
#view-workflow .wf-tool-dock { left: 84px; }

/* 3) Premium border on the top capsule: a brighter hairline with a lit top
      edge and a soft lift, the refined edge the reference has. */
.appbar-left,
.appbar-right {
  border: 1px solid rgba(255, 255, 255, 0.14) !important;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.12),
    inset 0 -1px 0 rgba(0, 0, 0, 0.25),
    0 10px 30px -12px rgba(0, 0, 0, 0.6) !important;
}
.appbar-left { border-right: 1px solid rgba(255, 255, 255, 0.08) !important; }

/* ==============================================================================
   GLOBAL PAGE COLOUR (data-app-tone on #app-shell) + APP-BAR CONTROL
   ------------------------------------------------------------------------------
   One control in the top bar sets the ground for every page. The four tones
   are the workflow canvas's four grounds, now global. Dark tones (onyx, slate)
   keep the app's light-on-dark ink; light tones (pewter, chalk) flip the
   ground-level text and the floating rail icons to dark so they stay legible. */
#app-shell { --app-ground: #060608; --app-dot: rgba(255,255,255,0.075); }
/* Height of the top frame/app bar — the boundary the Studio Agent starts below. */
#app-shell { --top-frame-height: 60px; }
#app-shell[data-app-tone="onyx"]   { --app-ground: #060608; --app-dot: rgba(255,255,255,0.075); }
#app-shell[data-app-tone="slate"]  { --app-ground: #1A1C20; --app-dot: rgba(255,255,255,0.085); }
#app-shell[data-app-lum="light"] { --app-ground: #9AA1A8; --app-dot: rgba(10,12,16,0.16); }
#app-shell[data-app-lum="light"]  { --app-ground: #EEF1F3; --app-dot: rgba(10,12,16,0.14); }

/* Light tones: re-ink what sits directly on the ground. */
#app-shell[data-app-lum="light"] :is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard, #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .panel-head h1,
#app-shell[data-app-lum="light"]  :is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard, #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .panel-head h1 {
  color: #101216 !important;
}
#app-shell[data-app-lum="light"] :is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard, #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .panel-head .panel-sub,
#app-shell[data-app-lum="light"]  :is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard, #view-shot-extractor, #view-remaster, #view-video, #view-avatar) .panel-head .panel-sub {
  color: rgba(16,18,22,0.72) !important;
}
/* Floating rail icons go dark on a light ground so they don't vanish. */
#app-shell[data-app-lum="light"] .rail-nav .rail-btn .rail-btn-icon,
#app-shell[data-app-lum="light"]  .rail-nav .rail-btn .rail-btn-icon {
  color: rgba(20,26,32,0.62) !important;
}
#app-shell[data-app-lum="light"] .rail-nav .rail-btn.active .rail-btn-icon,
#app-shell[data-app-lum="light"]  .rail-nav .rail-btn.active .rail-btn-icon {
  color: #0C0F13 !important;
}
#app-shell[data-app-lum="light"] .rail:hover .rail-btn-label,
#app-shell[data-app-lum="light"]  .rail:hover .rail-btn-label { color: #0C0F13; }

/* ---------- the app-bar control ---------- */
.appbar-tone { position: relative; display: flex; align-items: center; }
.appbar-tone-trigger {
  width: 30px; height: 30px; padding: 0; border: 0; border-radius: 999px;
  background: transparent; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.appbar-tone-trigger:hover { background: rgba(255,255,255,0.06); }
.appbar-tone-current {
  width: 16px; height: 16px; border-radius: 999px;
  background: #060608;
  box-shadow: 0 0 0 1px rgba(255,255,255,0.3), inset 0 0 0 1px rgba(255,255,255,0.15);
}
/* Canvas-theme control: a continuous lightness slider (0-100%) with the four
   named stops kept as quick-jumps. The slider drives --app-ground/--app-dot in
   JS; the swatches render the same ramp so the control reads as its own output.
   Centred under the menu so the top housing can expand around it when open. */
.appbar-tone-pop {
  /* Fixed + viewport-centred so the control block sits centred under the whole
     housing (the housing is centred in the viewport), not under the right-side
     trigger. One compact centred unit occupying only the middle of the tab. */
  position: fixed; top: 50px; left: 50%; transform: translateX(-50%);
  display: none; flex-direction: column; gap: 5px;
  width: 232px; padding: 6px 14px 7px;
  /* No surface of its own: the top housing expands to sit behind it, so the
     panel reads as part of the frame rather than a floating popover. */
  background: transparent;
  border: 0; border-radius: 16px; box-shadow: none;
  z-index: 60;
}
.appbar-tone.open .appbar-tone-pop { display: flex; }
.appbar-tone-head { display: flex; align-items: baseline; justify-content: space-between; }
.appbar-tone-heading { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.59375rem; letter-spacing: 0.16em; text-transform: uppercase; color: rgba(255,255,255,0.5); }
.appbar-tone-pct { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.625rem; letter-spacing: 0.08em; font-variant-numeric: tabular-nums; color: rgba(255,255,255,0.82); }
.appbar-tone-slider {
  -webkit-appearance: none; appearance: none; width: 100%; height: 16px; margin: 0; padding: 0; cursor: pointer;
  /* Ruler track: evenly spaced tick marks on a dark bed. */
  background:
    repeating-linear-gradient(90deg, rgba(255,255,255,0.16) 0, rgba(255,255,255,0.16) 1px, transparent 1px, transparent 9px),
    #0c0c0f;
  background-position: 4px center, 0 0;
  border-radius: 7px; border: 1px solid rgba(255,255,255,0.10);
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.6);
}
/* Thumb: a darker-silver vertical bar, taller than the ticks so it reads as the
   current position. */
.appbar-tone-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 6px; height: 26px; border-radius: 3px;
  background: linear-gradient(180deg, #a2a7ae 0%, #767c84 55%, #5f656d 100%);
  border: 1px solid rgba(0,0,0,0.4);
  box-shadow: 0 2px 6px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.35);
  cursor: grab;
}
.appbar-tone-slider::-webkit-slider-thumb:active { cursor: grabbing; }
.appbar-tone-slider::-moz-range-thumb {
  width: 6px; height: 26px; border-radius: 3px;
  background: linear-gradient(180deg, #a2a7ae 0%, #767c84 55%, #5f656d 100%);
  border: 1px solid rgba(0,0,0,0.4);
  box-shadow: 0 2px 6px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.35);
  cursor: grab;
}
.appbar-tone-slider:focus-visible { outline: 2px solid rgba(255,255,255,0.35); outline-offset: 3px; }
.appbar-tone-stops { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.app-tone { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 0; border: 0; background: transparent; cursor: pointer; }
.app-tone .app-tone-sw { width: 20px; height: 20px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.2); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06); transition: transform 0.12s ease, box-shadow 0.12s ease; }
.app-tone:hover .app-tone-sw { transform: scale(1.1); }
.app-tone.active .app-tone-sw { box-shadow: 0 0 0 2px rgba(255,255,255,0.92); border-color: transparent; }
.app-tone .app-tone-name { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.5rem; letter-spacing: 0.05em; text-transform: uppercase; color: rgba(255,255,255,0.42); }
.app-tone.active .app-tone-name { color: rgba(255,255,255,0.8); }

/* The tool dock clears the collapsed icon rail, but the rail expands on hover
   and would cover it - so the dock shifts right in step with the expansion,
   the same way the page content does. */
#view-workflow .wf-tool-dock { transition: left 0.16s ease; }
#app-shell:has(.rail:hover) #view-workflow .wf-tool-dock { left: 228px; }

/* Hand-tool dock: keep the two tools visibly TOGETHER as one unit. The dark
   override made the container nearly transparent on the canvas, so the two
   buttons read as split. A solid pill with a tight gap joins them. */
#view-workflow .wf-tool-dock {
  background: rgba(26, 26, 30, 0.96) !important;
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid rgba(255, 255, 255, 0.12) !important;
  gap: 2px;
  padding: 4px;
  border-radius: 14px;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.6) !important;
}
#view-workflow .wf-tool-btn { border-radius: 10px; }

/* The dock is now a full palette: pointer + hand, then the node types. A short
   hairline separates the two groups. */
#view-workflow .wf-tool-sep {
  width: 18px; height: 1px; margin: 3px auto;
  background: rgba(255, 255, 255, 0.14);
  flex: none;
}

/* ==============================================================================
   WORKFLOW FLOATING CHROME ADAPTS TO THE PAGE COLOUR
   ------------------------------------------------------------------------------
   The dock (and the other floating controls) were a fixed dark that nearly
   matched the slate ground - low contrast. They now flip with the page tone:
   a raised graphite that reads above the dark grounds (onyx, slate), and a
   dark fill that reads above the light grounds (pewter, chalk). Icons stay
   bright so they are visible on either. */
#view-workflow .wf-tool-dock,
#view-workflow .node-zoom-pill,
#view-workflow .node-zoom-side-btn,
#view-workflow .node-count-pill,
#view-workflow .node-canvas-fab {
  background: rgba(46, 48, 55, 0.97) !important;
  border: 1px solid rgba(255, 255, 255, 0.16) !important;
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.5) !important;
}
#app-shell[data-app-lum="light"] #view-workflow .wf-tool-dock,
#app-shell[data-app-lum="light"]  #view-workflow .wf-tool-dock,
#app-shell[data-app-lum="light"] #view-workflow .node-zoom-pill,
#app-shell[data-app-lum="light"]  #view-workflow .node-zoom-pill,
#app-shell[data-app-lum="light"] #view-workflow .node-zoom-side-btn,
#app-shell[data-app-lum="light"]  #view-workflow .node-zoom-side-btn,
#app-shell[data-app-lum="light"] #view-workflow .node-count-pill,
#app-shell[data-app-lum="light"]  #view-workflow .node-count-pill,
#app-shell[data-app-lum="light"] #view-workflow .node-canvas-fab,
#app-shell[data-app-lum="light"]  #view-workflow .node-canvas-fab {
  background: rgba(22, 22, 26, 0.95) !important;
  border-color: rgba(255, 255, 255, 0.12) !important;
}
/* dock icons bright on the raised/dark fill at every tone; active tool inverts */
#view-workflow .wf-tool-btn .wf-tool-icon .ph { color: rgba(255, 255, 255, 0.82) !important; }
#view-workflow .wf-tool-btn:hover .wf-tool-icon .ph { color: #ffffff !important; }
#view-workflow .wf-tool-btn:hover { background: rgba(255, 255, 255, 0.14); }
#view-workflow .wf-tool-btn.active { background: rgba(255, 255, 255, 0.92) !important; }
#view-workflow .wf-tool-btn.active .wf-tool-icon .ph { color: #17171b !important; }

/* ==============================================================================
   TOP-BAR SIZING, COLOUR BUTTONS, SAVE ROW ON THE TOP LINE, DOCK ROUNDED
   ============================================================================== */

/* Capsule sized closer to the reference: a touch slimmer and tighter. */
.appbar-left, .appbar-right { height: 36px; }
.appbar-icon-btn { width: 28px; height: 28px; }
.appbar-icon-btn .ph { width: 16px; height: 16px; }
.appbar-account { height: 30px; }
.appbar-account .account-avatar { width: 22px; height: 22px; }
.appbar-account .account-name { font-size: 0.6875rem; }
.appbar-cta { height: 28px; font-size: 0.71875rem; padding: 0 12px; }
.appbar-cta-icon { font-size: 0.8125rem; }
.appbar-logo { height: 20px; }

/* Colour (tone) buttons were too big. */
.appbar-tone-trigger { width: 26px; height: 26px; }
.appbar-tone-current { width: 12px; height: 12px; }

/* Undo / Redo / Save move up onto the top-bar line, top-right, clear of the
   centered capsule. They leave the workflow header row. */
#view-workflow.active .workflow-save-row {
  position: fixed;
  top: 12px; right: 16px;
  z-index: 45;
  display: flex; align-items: center; gap: 6px;
  margin: 0;
}
.wf-hist-btn {
  width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center;
}
.wf-hist-btn .ph { width: 15px; height: 15px; }

/* Dock: round the bottom to match the top (the top-only radius was for an old
   dock+palette join that no longer exists - the node tools live in the dock). */
#view-workflow #node-viewport-controls .wf-tool-dock,
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) ~ #node-viewport-controls .wf-tool-dock {
  border-radius: 16px !important;
  border-bottom: 1px solid rgba(255,255,255,0.16) !important;
  padding-bottom: 4px !important;
}

/* ==============================================================================
   SHOT EXTRACTOR & UPSCALE: CENTRED BESIDE THE RAIL, INDEPENDENT OF MENU STATE
   ------------------------------------------------------------------------------
   These wide two-panel views were left-aligned (so uncentred when the menu is
   collapsed) and only appeared centred because the hover-shift pushed them
   right. They now centre symmetrically: on wide screens the icon rail sits in
   the empty left gutter; on narrow screens a left inset clears it. The
   hover-shift is overridden here so the layout never depends on the menu. */
#view-shot-extractor.active,
#view-remaster.active {
  max-width: 1560px !important;
  margin-left: auto !important;
  margin-right: auto !important;
  padding-top: 78px !important;
  padding-bottom: 48px !important;
  padding-left: 40px !important;
  padding-right: 40px !important;
}
@media (max-width: 1740px) {
  #view-shot-extractor.active,
  #view-remaster.active {
    padding-left: 100px !important;
    padding-right: 24px !important;
  }
}

/* Same symmetric centring for the other wide two-panel views (and Image /
   Character Sheet, which share the layout): centre in the space beside the
   rail with equal padding, independent of the menu. Each keeps its own
   max-width; only the horizontal padding and the hover-shift are overridden.
   Below 1740px a left inset clears the rail. */
#view-storyboard.active, #view-video.active, #view-avatar.active,
#view-image.active, #view-charsheet.active {
  margin-left: auto !important;
  margin-right: auto !important;
  padding-left: 40px !important;
  padding-right: 40px !important;
}
@media (max-width: 1740px) {
  #view-storyboard.active, #view-video.active, #view-avatar.active,
  #view-image.active, #view-charsheet.active {
    padding-left: 100px !important;
    padding-right: 24px !important;
  }
}

/* ==============================================================================
   UNIFORM GROUND ON EVERY CONTENT PAGE (including Prompt Vault)
   ------------------------------------------------------------------------------
   The nine generation views had the #060608 dot ground; the rest (Vault,
   Collections, Moodboards, Elements, Explore, Projects, Contact) had a
   transparent main with dots floating over an off-black body, or their own
   opaque fill (Vault's #0a0a0c) - so grounds and colours did not match. These
   pages now paint the SAME tone-driven ground, full-bleed, and any page that
   carried its own opaque fill is made transparent so the ground shows through. */
main:has(#view-studio-hub.active),
main:has(#view-vault.active),
main:has(#view-collections.active),
main:has(#view-moodboards.active),
main:has(#view-elements.active),
main:has(#view-explore.active),
main:has(#view-projects.active),
main:has(#view-contact.active) {
  background:
    radial-gradient(var(--app-dot, rgba(255,255,255,0.075)) 1.3px, transparent 1.3px) 0 0 / 22px 22px,
    var(--app-ground, #060608) !important;
  background-attachment: fixed, fixed !important;
}
/* Vault carries its OWN opaque dotted ground (not a transparent view) - it
   still shows the page bleed, but the page is not transparent. */
#view-vault.active {
  background:
    radial-gradient(var(--app-dot, rgba(255,255,255,0.075)) 1.3px, transparent 1.3px) 0 0 / 22px 22px,
    var(--app-ground, #060608) !important;
  /* local, not fixed: the vault view already sits inside main (which paints a
     fixed ground for other pages). Two stacked fixed layers repaint out of
     sync on layout changes - the transparency glitch. One non-fixed layer on
     the view, and the vault is dropped from main's ground below. */
  background-attachment: local, local !important;
}

/* ==============================================================================
   RE-CENTRE ON MENU EXPAND
   ------------------------------------------------------------------------------
   When the rail expands on hover, padding MAIN from the left shrinks the
   content box, so every centred view re-centres in the space beside the wider
   rail instead of staying put (or being overlapped). The ground uses
   background-attachment:fixed, so it stays full-bleed under the padding.
   Workflow is excluded - its canvas must remain edge-to-edge - and Home is the
   landing. Only applies on desktop, where the rail is a hover overlay. */
@media (min-width: 861px) {
  main { transition: padding-left 0.18s ease; }
  #app-shell:has(.rail:hover) main:has(> .view.active:not(#view-workflow):not(#view-home)) {
    padding-left: 234px !important;
  }
}

/* ==============================================================================
   PROMPT VAULT: transparent category bar, single dot layer
   ------------------------------------------------------------------------------
   The category bar carried a solid rgba(8,8,9,.85) strip that ended at the
   content's left edge - a tacky cut on the ground. It goes transparent so the
   pills float on the ground. A blur is kept so scrolled cards read softly
   behind it. */
#view-vault .vault-catbar,
#view-vault .topbar {
  background: transparent !important;
  border-bottom: 0 !important;
}

/* Extend the uniform ground to the remaining account pages so removing the
   per-canvas dot layer below leaves them on the same ground, not blank. */
main:has(#view-settings.active),
main:has(#view-profile.active),
main:has(#view-admin.active) {
  background:
    radial-gradient(var(--app-dot, rgba(255,255,255,0.075)) 1.3px, transparent 1.3px) 0 0 / 22px 22px,
    var(--app-ground, #060608) !important;
  background-attachment: fixed, fixed !important;
}

/* Double dots: these canvases each painted their own ::before dot grid, which
   now overlaps main's ground dots. main is the single source of the ground, so
   the per-canvas dot layer is removed. */
.projects-canvas::before, .elements-canvas::before, .admin-canvas::before,
.settings-canvas::before, .profile-canvas::before, .explore-canvas::before,
.vault-canvas::before {
  background-image: none !important;
}

/* The transparent category bar still ran backdrop-filter:blur, which smeared
   the dots behind it into a darker band - the "divider black bar". Blur off,
   and the prev/next toggles go from dark blocks to subtle outlined buttons so
   nothing reads as a bar across the header. */
#view-vault .vault-catbar,
#view-vault .topbar {
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}
/* Toggles: icon-only, no box - a subtle bg/border read as an empty placeholder
   field (worst on the enabled right one). Just the chevron, with a hover well. */
#view-vault .vault-catnav {
  background: transparent !important;
  border: 0 !important;
  color: rgba(255, 255, 255, 0.55) !important;
}
#view-vault .vault-catnav:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.08) !important;
  color: #fff !important;
}

/* A dark base on the shell so any momentary transparency (page switch, a
   repaint) shows the same near-black, never the silver body behind it. */
#app-shell:not(.landing-active) { background: #060608; }

/* Studio hub: same as the other content pages - its own opaque dotted ground
   (was plain black), non-fixed so main's fixed layer behind it is single. main
   is on the clean ground list above, so no scanline grain shows when the menu
   expands and shifts the view. */
#view-studio-hub.active {
  background:
    radial-gradient(var(--app-dot, rgba(255,255,255,0.075)) 1.3px, transparent 1.3px) 0 0 / 22px 22px,
    var(--app-ground, #060608) !important;
  background-attachment: local, local !important;
}

/* ==============================================================================
   STUDIO HUB: NO LEFT RAIL (the tiles ARE the section nav), keep the top bar
   ------------------------------------------------------------------------------
   The hub's tiles duplicate the rail, so the rail is hidden here and the tile
   grid uses the full width. The top bar stays for account, New Project, page
   colour and Settings, so nothing global is stranded. The rail returns on the
   actual work pages. */
#app-shell:has(#view-studio-hub.active) {
  grid-template-columns: minmax(0, 1fr) !important;
  grid-template-areas: "top" "main" !important;
}
#app-shell:has(#view-studio-hub.active) .rail { display: none !important; }
/* no rail here, so no left clearance and no hover recenter */
#view-studio-hub.active {
  padding-left: 44px !important;
  padding-right: 44px !important;
}

/* The node tools (Image/Video/Prompt/Reference) now live in the tool dock, so
   the old quick-add row - which repositioned into a second vertical bar once
   nodes existed - is redundant there and read as a double toolbar. It stays as
   the centered empty-state helper (no nodes), and is hidden once nodes exist. */
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-row {
  display: none !important;
}

/* ==============================================================================
   APP BAR: PREMIUM GRADIENT RIM (like the reference)
   ------------------------------------------------------------------------------
   A flat 1px border reads cheap. This draws the border as a masked gradient -
   bright at the top-left, fading through, catching light again at the bottom -
   so the capsule edge reads like a polished metal/glass rim. The mask keeps it
   to a 1px ring that follows the rounded corners. A faint outer glow and the
   existing lift complete the float. */
.appbar-left,
.appbar-right {
  position: relative;
  border: 0 !important;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.14),
    0 8px 26px -10px rgba(0, 0, 0, 0.6),
    0 0 22px -8px rgba(180, 200, 230, 0.10) !important;
}
.appbar-left::before,
.appbar-right::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(150deg,
    rgba(255, 255, 255, 0.30) 0%,
    rgba(255, 255, 255, 0.06) 38%,
    rgba(255, 255, 255, 0.02) 62%,
    rgba(255, 255, 255, 0.16) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  pointer-events: none;
  z-index: 1;
}
/* the seam between the two halves stays a hairline, not a doubled rim */
.appbar-left::before  { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.appbar-right::before { border-top-left-radius: 0;  border-bottom-left-radius: 0; }

/* ==============================================================================
   TOP SCRIM: a premium darkened band at the top of the workspace
   ------------------------------------------------------------------------------
   The reference grounds its floating top capsule with a soft darkened band at
   the very top of the canvas that fades down - the "line" is that band's lower
   edge. A fixed gradient overlay sits above the ground/content but below the
   rail and app bar, so the capsule floats over it. Subtle on the dark tones,
   clearly premium on the lighter ones - exactly as the reference reads. */
#app-shell:not(.landing-active)::before {
  content: "";
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 86px;
  background: linear-gradient(180deg,
    rgba(0, 0, 0, 0.34) 0%,
    rgba(0, 0, 0, 0.14) 55%,
    transparent 100%);
  pointer-events: none;
  z-index: 22;
}

/* ==============================================================================
   TOP FRAME — a centered HANGING TAB molded into the workspace top edge.
   ------------------------------------------------------------------------------
   The top edge runs flat, then curves down on broad shoulders into a wider
   black housing that hangs below the line and holds the centered menu; on the
   right it curves back up to the flat edge. It is ONE continuous silhouette,
   not a line wrapping the pill. Two SVG paths, both generated in
   initAppbarFrame() from the live menu geometry (so the housing is wider than
   the controls at any desktop width):
     .frame-fill  the filled housing body (sits BEHIND the controls)
     .frame-line  the top-edge + shoulder silhouette stroke (top left open,
                  so no line runs behind the menu)
   Sits below the menu pills (z 39) so the controls nest inside the housing. */
.appbar-frame {
  position: fixed;
  top: 0; left: 0;
  width: 100%;
  height: 96px;
  pointer-events: none;
  z-index: 39;
  overflow: visible;
  display: none;
}
#app-shell:not(.landing-active) .appbar-frame { display: block; }
.appbar-frame .frame-fill {
  fill: var(--frame-housing, #101015);
  stroke: none;
  filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.32));
  transition: fill 200ms ease;
}
.appbar-frame .frame-line {
  fill: none;
  stroke: var(--frame-line, rgba(255, 255, 255, 0.14));
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  shape-rendering: geometricPrecision;
  transition: stroke 200ms ease;
}
/* The housing stays a dark charcoal on every tone (a "black menu housing");
   the silhouette line stays restrained on dark, a touch stronger on light. */
#app-shell { --frame-housing: #101015; --frame-line: rgba(255, 255, 255, 0.14); }
#app-shell[data-app-lum="light"] { --frame-housing: #17191f; --frame-line: rgba(14, 18, 24, 0.22); }

/* ==============================================================================
   INNER PILL REMOVED — controls float directly inside the hanging tab.
   ------------------------------------------------------------------------------
   The outer .appbar-frame is the housing now, so the two menu halves give up
   their own capsule entirely: background, border, gradient rim (::before),
   shadow, blur and radius all go. Only their layout is kept — height, padding,
   gap, flush centering — so every control keeps its position, order, spacing,
   hover/active states and click behaviour. Not another pill; just the controls
   sitting on the tab's dark surface as one compact floating toolbar. */
.appbar-left,
.appbar-right {
  background: transparent !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}
.appbar-left::before,
.appbar-right::before { display: none !important; }

/* ==============================================================================
   CANVAS TOOL DOCK — compact premium utility strip (final pass)
   ------------------------------------------------------------------------------
   The floating vertical tool palette becomes a quiet, tight utility dock:
   a near-black graphite surface (no glass blur), a thin border, a soft shadow,
   smaller buttons and normalized icons. The active tool keeps the single strong
   fill. Only the tool palette is touched — not the zoom/count pills, the nav
   rail, the canvas, or any tool behaviour. */
#view-workflow .wf-tool-dock {
  /* Plumar-style: a restrained translucent strip the canvas shows through,
     with only a mild blur — not the near-solid graphite bar. The active tool
     keeps its own fill (separate rule), so selection stays clear. */
  background: rgba(12, 12, 15, 0.62) !important;
  border: 1px solid rgba(255, 255, 255, 0.08) !important;
  -webkit-backdrop-filter: blur(8px) saturate(120%) !important;
  backdrop-filter: blur(8px) saturate(120%) !important;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22) !important;
}
#view-workflow .wf-tool-btn { width: 30px !important; height: 30px !important; }
#view-workflow .wf-tool-btn .wf-tool-icon .ph { width: 16px !important; height: 16px !important; }
#view-workflow .wf-tool-sep { width: 16px !important; margin: 2px auto !important; }

/* ==============================================================================
   TOOL DOCK -> HORIZONTAL, BOTTOM-CENTRE (Plumar-style)
   ------------------------------------------------------------------------------
   The palette moves off the left edge to a compact horizontal dock floating at
   the bottom-centre of the canvas. High-specificity + !important so it beats the
   earlier left-position, rail-hover-shift and top-radius rules. Orientation and
   position only; the compact treatment, tools, and behaviour are unchanged. The
   bottom-right zoom controls are untouched (this dock is centred, they are on
   the right, so they never stack). */
#app-shell #view-workflow #node-viewport-controls .wf-tool-dock,
#app-shell:has(.rail:hover) #view-workflow #node-viewport-controls .wf-tool-dock {
  /* Anchor to the canvas overlay box, which is now sized to the VISIBLE
     viewport (see #view-workflow.active min-height:100dvh). The earlier
     position:fixed here assumed fixed anchors to the visual viewport — but on
     iOS Safari fixed anchors to the TALL layout viewport, so the dock still
     landed below the visible bottom in landscape. position:absolute pins it to
     #node-viewport-controls (inset:0 of the dvh-sized canvas), so bottom sits a
     fixed gap above the real visible edge. The overlay is a non-panning sibling
     of #node-canvas, so absolute never drifts with canvas pan/zoom.
     left:50% centres on the full-width canvas — no rail offset needed. */
  position: absolute !important;
  left: 50% !important;
  right: auto !important;
  top: auto !important;
  /* Clear the home indicator in landscape (the dock is bottom-centre, right
     where the indicator sits) and never crowd the visible bottom edge. */
  bottom: max(22px, calc(env(safe-area-inset-bottom, 0px) + 14px)) !important;
  transform: translateX(-50%) !important;
  flex-direction: row !important;
  align-items: center !important;
  border-radius: 13px !important;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
  padding: 4px !important;
  gap: 2px !important;
}
/* Divider becomes a vertical hairline between the two tool groups. */
#view-workflow #node-viewport-controls .wf-tool-sep {
  width: 1px !important;
  height: 16px !important;
  margin: 0 3px !important;
}
/* Shortcut badges (V / H) tuck to the button's bottom-right without crowding
   the neighbour in the horizontal row. */
#view-workflow #node-viewport-controls .wf-tool-key {
  right: -2px !important;
  bottom: -2px !important;
}

/* NOTE (updated): the workflow height chain now uses 100dvh for
   #view-workflow.active, and the dock is position:absolute inside the dvh-sized
   canvas overlay (both above). The earlier concern was that dvh "jumps" as the
   toolbar shows/hides — but that only happens on a document that scrolls; the
   workflow view never document-scrolls (the canvas pans internally) and iPad
   keeps a persistent tab bar, so dvh is stable here. This is what actually
   keeps the quick-add row centered and the tool dock on screen in landscape;
   the previous position:fixed approach failed because iOS anchors fixed to the
   tall LAYOUT viewport, not the visible one. */

/* ==============================================================================
   STANDALONE-PAGE CLOSE BUTTON (Contact, Settings) — exit back to the last page.
   Sits top-right of the page header, aligned with the title. */
#view-contact .panel-head, #view-settings .panel-head { position: relative; }
.view-close-btn {
  position: absolute; top: 0; right: 0;
  width: 34px; height: 34px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.12);
  color: rgba(255, 255, 255, 0.6);
  cursor: pointer; z-index: 5;
  transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
.view-close-btn:hover {
  background: rgba(255, 255, 255, 0.12); color: #fff; border-color: rgba(255, 255, 255, 0.22);
}
/* On a light ground the button and glyph invert so they stay legible. */
#app-shell[data-app-lum="light"] .view-close-btn {
  background: rgba(10, 12, 16, 0.05); border-color: rgba(10, 12, 16, 0.18); color: rgba(10, 12, 16, 0.6);
}
#app-shell[data-app-lum="light"] .view-close-btn:hover {
  background: rgba(10, 12, 16, 0.1); color: #101216; border-color: rgba(10, 12, 16, 0.28);
}

/* Contact page: centre the form column so it isn't left-aligned with a large
   empty right side — consistent with the app's other centred content. */
#view-contact .projects-canvas.settings-canvas {
  max-width: 820px; margin-left: auto; margin-right: auto;
}

/* ==============================================================================
   BOTTOM-CENTRE TOOL DOCK — Plumar proportions (substantial, not tiny).
   Larger buttons/icons, more breathing room and inter-tool spacing; position,
   orientation, tools and behaviour unchanged. */
#app-shell #view-workflow #node-viewport-controls .wf-tool-dock,
#app-shell:has(.rail:hover) #view-workflow #node-viewport-controls .wf-tool-dock {
  gap: 7px !important;
  padding: 6px 12px !important;
  border-radius: 15px !important;
}
#view-workflow #node-viewport-controls .wf-tool-btn {
  width: 36px !important; height: 36px !important; border-radius: 11px !important;
}
#view-workflow #node-viewport-controls .wf-tool-btn .wf-tool-icon .ph {
  width: 18px !important; height: 18px !important;
}
#view-workflow #node-viewport-controls .wf-tool-sep {
  width: 1px !important; height: 20px !important; margin: 0 5px !important;
}

/* ==============================================================================
   TOOL DOCK vs. CENTRED QUICK-ADD — one or the other, by canvas state.
   Empty canvas: the centred Image/Video/Prompt/Reference quick-add buttons (and
   hint) are the way in; the bottom tool dock stays hidden. Once the first node
   exists: the bottom dock appears and the quick-add group hides (the dock
   carries the same node tools, so nothing is lost). */
/* Empty board shows the four centred quick-add boxes; once the first node
   exists they hide and the bottom dock takes over as the way to add nodes. */
#view-workflow .node-canvas-wrap:has(.node-canvas:not(.has-nodes)) #node-viewport-controls .wf-tool-dock {
  display: none !important;
}
#view-workflow .node-canvas-wrap:has(.node-canvas.has-nodes) .wf-quickadd-row {
  display: none !important;
}

/* Profile also gets the standalone-page close button. */
#view-profile .panel-head { position: relative; }

/* ==============================================================================
   CONTACT PAGE — full-bleed dotted ground + centred form (matches the
   generation pages). main goes edge-to-edge so the fixed dotted ground bleeds
   the whole width; the form column is centred inside it. */
main:has(> #view-contact.active) {
  max-width: none !important;
  margin-left: 0 !important;
  margin-right: 0 !important;
  padding: 0 !important;
}
#view-contact.active {
  box-sizing: border-box;
  width: 100%;
  padding: 76px 44px 60px;
  margin-left: auto;
  margin-right: auto;
}

/* ==============================================================================
   TOP-FRAME MOVING SHINE — a polished-silver specular that traces the exact
   border geometry (the same path as .frame-line) and loops continuously.
   A short bright dash animated by stroke-dashoffset; pathLength=100 makes it
   resolution-independent and the loop seamless (dash period == pathLength).
   Base border stays visible underneath. Pure CSS/SVG, compositor-friendly. */
.appbar-frame .frame-shine {
  fill: none;
  stroke: var(--frame-shine, rgba(232, 240, 251, 0.96));
  stroke-width: 2;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  shape-rendering: geometricPrecision;
  stroke-dasharray: 17 83;   /* short specular + long gap, sums to pathLength */
  stroke-dashoffset: 0;
  opacity: 0;                /* revealed + animated by JS (Web Animations API),
                                which is not disabled by the app-wide
                                reduced-motion CSS reset; JS still gates on the
                                user's real motion preference via matchMedia. */
  will-change: stroke-dashoffset;
}
@keyframes appbar-frame-shine {
  from { stroke-dashoffset: 100; }
  to   { stroke-dashoffset: 0; }
}
/* Light ground: a cleaner, brighter specular so it still reads. */
#app-shell[data-app-lum="light"] { --frame-shine: rgba(255, 255, 255, 0.95); }
/* Reduced motion: stop the trace, keep only the static base border. */
@media (prefers-reduced-motion: reduce) {
  .appbar-frame .frame-shine { animation: none; opacity: 0; }
}

/* ==============================================================================
   MOBILE PASS (<=860px): visible menu button, centred pages, agent icon
   ------------------------------------------------------------------------------
   Appended late so its !important rules win the source-order tie against the
   legacy mobile rules above (the absolute-positioned toggle and the
   max-width:1740px rail insets). Three fixes:

   1) #rail-toggle was position:absolute (legacy) + faint 6%-white, so it sat
      UNDER the BETA badge and read as invisible. It already opens the drawer
      (.rail.open, wired in app.js) — only its look/placement were wrong. Make
      it a clean in-flow brushed-silver button at the far LEFT of the app bar,
      before the logo, so it reads as the collapsed-menu handle.
   2) The generation views carry padding-left:100px below 1740px to clear the
      desktop icon rail. On mobile the rail is a slide-in drawer, so that inset
      just shoves content off-centre. Reset to symmetric 16px so pages centre.
   3) .sd-tab-icon sizes the new inline agent sparkle SVG in both collapsed
      launchers (replaces the font-dependent glyph that rendered as a bare
      circle). This one is not width-gated — the desktop edge tab uses it too. */

@media (max-width: 860px) {
  /* 1 — the collapsed-menu button, far left, clearly visible */
  .appbar-left { position: relative; }
  #rail-toggle.rail-toggle {
    display: flex !important;
    position: static !important;
    order: -1;
    transform: none !important;
    top: auto !important; right: auto !important; left: auto !important; bottom: auto !important;
    margin: 0 10px 0 0 !important;
    width: 38px; height: 38px;
    flex: 0 0 auto;
    border-radius: 11px;
    background: linear-gradient(160deg, #f4f5f6 0%, #d6d8db 45%, #b7bac0 100%) !important;
    color: #1b1b1e !important;
    border: 1px solid rgba(10, 10, 10, 0.35) !important;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.55) !important;
    font-size: 1.0625rem;
  }
  #rail-toggle.rail-toggle:hover { filter: brightness(1.04); }

  /* The app bar centres its content on desktop (to seat the notch capsule);
     on mobile that centring makes the overcrowded row spill off BOTH edges,
     hiding the left group. Pin the groups to the edges instead and trim the
     left group's padding so the menu button stays flush-left and on-screen. */
  .appbar { justify-content: space-between !important; }
  .appbar-left { padding-left: 0 !important; padding-right: 0 !important; gap: 6px !important; }

  /* 2 — centre EVERY content page on mobile. The desktop icon-rail inset
     (~96px on the left, per-view) leaks all the way down to phone widths and
     shoves every page off-centre. Reset them all to symmetric 16px so the
     content centres. Workflow is excluded — its node canvas is intentionally
     full-bleed edge-to-edge. High enough specificity (+ !important, appended
     late) to beat every per-view desktop inset. */
  main .view.active:not(#view-workflow),
  .view.active:not(#view-workflow) {
    padding-left: 16px !important;
    padding-right: 16px !important;
    margin-left: auto !important;
    margin-right: auto !important;
    max-width: 100% !important;
  }
}

/* 3 — Studio Agent launcher icon (both collapsed launchers). A person mark
   (agent), not the sparkle. Colour is set per launcher so it reads on each
   ground: near-black on the light mobile circle (was inheriting a blue accent),
   and the tab's own light text colour on the dark desktop edge tab. */
.sd-tab-icon { display: block; width: 22px; height: 22px; }
.sd-mobile-launcher .sd-tab-icon { width: 26px; height: 26px; }
.sd-mobile-launcher { color: #18181b !important; }

/* Brand logo in two places (built by scripts/brand/build_brand.py: the mark,
   then SHOTBORN): the left end of the top menu bar, and the top of the left
   menu. In the left menu the mark is as tall as a menu button is wide, so the
   collapsed 40px column shows the mark alone and opening the menu reveals the
   wordmark beside it, with no second image to swap. */
.appbar-logo { display: block; height: 30px; width: auto; }
.rail-logo {
  flex: none; height: 40px; width: 100%; margin: 6px 0 14px;
  overflow: hidden; pointer-events: none;
}
.rail-logo-img { display: block; height: 40px; width: auto; max-width: none; }
@media (max-width: 860px) {
  .rail-logo { margin: 8px 0 16px 8px; }
}

/* ==============================================================================
   MOBILE PASS 2 (<=860px): page scroll + a solid, scrimmed menu drawer
   ------------------------------------------------------------------------------
   Two more mobile-only fixes, appended late so their !important wins:

   A) SCROLL. The desktop shell pins html/body to the viewport height and scrolls
      an inner container; on mobile that trapped the page, so anything below the
      fold (e.g. the Avatar page's lower presets and its Generate button) was
      unreachable. Let the document scroll naturally instead.

   B) DRAWER. The open menu was a TRANSPARENT full-width .rail with the opaque
      panel floating at 240px (leftover desktop icon-rail behaviour), so the page
      bled straight through it. Make the rail itself a solid ~300px drawer, let
      the panel fill and scroll inside it (so every nav item is reachable), and
      cast a full-viewport scrim with a spread box-shadow so the page behind is
      dimmed rather than revealed. Closing stays on the app-bar menu button. */
@media (max-width: 860px) {
  /* A — natural document scroll */
  html, body { height: auto !important; min-height: 100% !important; overflow: visible !important; }
  #app-shell { height: auto !important; min-height: 100vh !important; overflow: visible !important; }
  main { height: auto !important; overflow: visible !important; }

  /* B — solid, scrimmed, scrollable drawer */
  .rail { width: min(300px, 86vw) !important; background: #0f0f12 !important; }
  .rail.open { box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.6) !important; }
  .rail-panel {
    position: static !important; top: auto !important; left: auto !important;
    width: 100% !important; height: 100% !important; max-height: none !important;
    background: transparent !important; overflow-y: auto !important;
    padding: 10px 0 28px !important;
  }
}

/* ==============================================================================
   STUDIO HUB — wider horizontal distribution of the existing 4-column grid
   ------------------------------------------------------------------------------
   Wide desktop only (>=1520px, where the four-column row already appears).
   The four EXISTING columns are pushed farther toward the page sides by
   widening the frame and letting four equal 1fr columns fill it; each 330px
   card keeps its exact size, centered in its (now wider) column. Nothing else
   changes — card/icon size, column count, row (vertical) spacing and the card
   design are all untouched, an incomplete last row still lands in columns 1-3,
   and narrower widths keep the existing responsive auto-fit behaviour. Scoped
   to #view-studio-hub so the Project hub is unaffected. */
@media (min-width: 1520px) {
  /* Pass 2 — a subtle further widening (~8-12% more grid footprint). At these
     widths the frame is capped by the VIEW's own 44px side padding, so widening
     the frame alone can't help — the inset itself has to come down. Trim the
     view's horizontal padding, then give .panel-head an equal compensating
     left padding so the "Studio" heading stays exactly where it was (nothing
     else moves). The frame loses its own side padding and its max-width rises,
     but is still capped so ultra-wide screens never run edge-to-edge. Four equal
     1fr columns fill the wider frame; each 330px card keeps its exact size,
     centered in its column. Card/icon size, column count, row spacing, card
     design, labels and the last-row column alignment are all untouched. */
  #view-studio-hub.active { padding-left: 28px !important; padding-right: 28px !important; }
  /* View padding drops 44->28; .panel-head carries 44px of its own left
     padding, so bump it to 60 (28+60 = 88) to hold the "Studio" heading at the
     exact same x it had before this pass. */
  #view-studio-hub .panel-head { padding-left: 60px; }
  #view-studio-hub .studio-frame { max-width: 1900px; padding-left: 0; padding-right: 0; }
  #view-studio-hub .project-hub-grid {
    grid-template-columns: repeat(4, 1fr);
    justify-items: center;
  }
  #view-studio-hub .project-hub-tile { width: 330px; }
}

/* ==============================================================================
   SHOTBORN focused fixes — pricing ground + left-rail balance
   ============================================================================== */

/* 1. Pricing page ground: match the rest of the app (near-black + dot grid)
   instead of the gray "settings-canvas" gradient it shared with Settings. Uses
   the same --app-ground / --app-dot tokens every other workspace page uses, so
   it also tracks the canvas-theme slider. Scoped to pricing only; Settings
   keeps its own treatment, and no pricing card/plan/billing styles change. */
main:has(> #view-pricing.active) {
  background:
    radial-gradient(var(--app-dot, rgba(255,255,255,0.075)) 1px, transparent 1px) 0 0 / 22px 22px,
    var(--app-ground, #060608) !important;
  background-attachment: local, local !important;
}

/* 3. Left rail vertical balance: with the top logo gone the nav sat against the
   top edge. .rail-panel is a full-height flex column holding only .rail-nav
   (which flex-shrinks and scrolls internally when it can't fit), so centering
   the panel's content balances the nav vertically when it fits and still
   scrolls cleanly when it doesn't — no arbitrary margins. Desktop only; the
   mobile drawer stays a top-aligned scrollable list. */
@media (min-width: 861px) {
  /* The nav column fills the panel (flex:1) and scrolls internally when tall,
     so centering the PANEL did nothing — the icon stack still sat at the top of
     the filled nav. Centre the icon group WITHIN the nav instead. `safe` keeps
     the top icons reachable (falls back to top-align + scroll) if the rail is
     ever shorter than the full stack. */
  .rail-nav { justify-content: safe center; }
}

/* Library picker grid: a narrow, visible scrollbar so it reads as scrollable
   without being heavy. */
.sd-lib-grid { scrollbar-width: thin; scrollbar-color: rgba(180,186,189,0.35) transparent; }
.sd-lib-grid::-webkit-scrollbar { width: 8px; }
.sd-lib-grid::-webkit-scrollbar-thumb { background: rgba(180,186,189,0.35); border-radius: 8px; }
.sd-lib-grid::-webkit-scrollbar-track { background: transparent; }

/* ==============================================================================
   SCENE workspace — centre in the space left of the (fixed) rail
   ------------------------------------------------------------------------------
   The rail is a fixed overlay and <main> is full viewport width, so the scene
   composition was centred against the WHOLE viewport and ignored the rail —
   leaving a big dead gutter when the rail is collapsed and overlapping the rail
   when it is pinned. Instead, inset the composition by the rail's actual
   occupied width plus an equal gutter on both sides, so it sits centred in the
   space that's genuinely available and WIDENS into the freed room when the rail
   collapses. --rail-w tracks the rail's real width (collapsed vs pinned); no
   fixed magic offsets on the view itself. Desktop only — mobile keeps its own
   full-width padding. Scene page only. */
#app-shell { --rail-w: 68px; }
#app-shell.rail-pinned { --rail-w: 222px; }
@media (min-width: 861px) {
  #view-scene.active {
    max-width: none;
    margin-left: 0; margin-right: 0;
    padding-left: calc(var(--rail-w) + 44px) !important;
    padding-right: 44px !important;
    transition: padding-left 0.16s ease;
  }
}

/* ==============================================================================
   STUDIO PAGE — always the black canvas + no Canvas Theme control
   ------------------------------------------------------------------------------
   The Studio hub's metallic icon cards are designed against the black ground; a
   light/gray canvas exposes their square image bounds. Force the black
   ground/dot on the Studio hub ONLY, overriding the inline --app-ground/--app-dot
   the canvas-theme slider writes on #app-shell (CSS !important beats inline). The
   user's global preference (those inline vars) is untouched, so every OTHER page
   keeps the chosen colour — leaving Studio and returning restores it. The Canvas
   Theme control is also hidden here; the app bar's flex layout closes the gap
   cleanly with no placeholder. Icons/cards/layout are not touched. */
#app-shell:has(#view-studio-hub.active) {
  --app-ground: #060608 !important;
  --app-dot: rgba(255, 255, 255, 0.075) !important;
}
#app-shell:has(#view-studio-hub.active) .appbar-tone { display: none !important; }

/* ==============================================================================
   Studio Agent on Workflow — overlay, don't crop the canvas
   ------------------------------------------------------------------------------
   Everywhere else the docked panel makes room by pushing #app-shell 380px left
   (#app-shell.sd-open margin-right). On Workflow that "crop" retreats the dotted
   node-canvas, so the 380px column behind the panel becomes bare #app-shell —
   and because the panel starts below the shining border, its top 60px showed as
   a black band tied to the chat (open = shows, close = gone). Root fix: on
   Workflow, do NOT crop. Keep the canvas full-width and let the fixed panel
   overlay it, so the dot grid stays continuous straight up to the frame with the
   panel sitting on top — no bare column, no black band, nothing painted on. */
#app-shell.sd-open:has(#view-workflow.active) { margin-right: 0 !important; }

/* ==============================================================================
   Scene LEFT column — one shared vertical rhythm (three tokens)
   ------------------------------------------------------------------------------
   The left column mixed four spacing sources (col-main flex-gap 24, group
   margin-top 34 + padding-top 32, panel margin-bottom 30, subgroup margin-top
   28) plus block-margin collapsing, so section transitions ran 121px at group
   boundaries but only 30px at the subgroup — no consistent rhythm. Replace all
   of it with three tokens. Blocks become flex columns so child margins are
   exact (flex items don't collapse). Panel heights, padding, and the 1px
   section dividers are untouched — this only positions the blocks.

     --scene-label-gap    label → its own panel      (smallest)
     --scene-panel-gap    panel → panel, same section (medium)
     --scene-section-gap  panel → next section label  (largest; same everywhere)
============================================================================== */
#view-scene {
  --scene-label-gap: 16px;
  --scene-panel-gap: 30px;
  --scene-section-gap: 46px;
}
#view-scene .scene-col-main { gap: 0; }
#view-scene .scene-col-main .scene-group,
#view-scene .scene-col-main .scene-subgroup {
  display: flex;
  flex-direction: column;
  gap: 0;
}
/* zero the ad-hoc panel margins that leaked into the transitions */
#view-scene .scene-col-main .scene-section,
#view-scene .scene-col-main .scene-visual-block,
#view-scene .scene-col-main .scene-visual-block-last { margin-top: 0; margin-bottom: 0; }
/* LABEL → its panel */
#view-scene .scene-col-main .scene-group-title,
#view-scene .scene-col-main .scene-subgroup-title { margin-bottom: var(--scene-label-gap); }
/* PANEL → PANEL (same section) */
#view-scene .scene-col-main .scene-section + .scene-section,
#view-scene .scene-col-main .scene-visual-block + .scene-section,
#view-scene .scene-col-main .scene-visual-block + .scene-visual-block { margin-top: var(--scene-panel-gap); }
/* SECTION TRANSITION — subgroup boundary (no divider): whole gap in margin */
#view-scene .scene-col-main .scene-subgroup,
#view-scene .scene-col-main .scene-subgroup + .scene-subgroup {
  margin-top: var(--scene-section-gap);
  padding-top: 0;
  border-top: 0;
}
/* SECTION TRANSITION — group boundary (keeps its 1px divider): split the same
   total gap above/below the line so the rhythm matches the subgroup */
#view-scene .scene-col-main .scene-group + .scene-group {
  margin-top: calc(var(--scene-section-gap) / 2);
  padding-top: calc(var(--scene-section-gap) / 2);
}

/* ==============================================================================
   Workflow bottom controls — one translucent system
   ------------------------------------------------------------------------------
   The center tool dock is a restrained translucent strip (rgba(12,12,15,0.62) +
   mild blur), but the bottom-LEFT status pill and bottom-RIGHT zoom pill / +−
   buttons were near-solid graphite (rgba(46,48,55,0.97) !important) and the
   minimap sat at 0.8 — so the three groups didn't read as one. Give them the
   SAME surface as .wf-tool-dock so the canvas faintly shows through all three.
   Later than the 0.97 rule, so it wins; positions/sizes/behaviour untouched. */
#view-workflow .node-count-pill,
#view-workflow .node-zoom-pill,
#view-workflow .node-zoom-side-btn,
#view-workflow .node-minimap {
  background: rgba(12, 12, 15, 0.62) !important;
  border: 1px solid rgba(255, 255, 255, 0.08) !important;
  -webkit-backdrop-filter: blur(8px) saturate(120%) !important;
  backdrop-filter: blur(8px) saturate(120%) !important;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22) !important;
}
/* hover/active on the interactive right-side controls — a touch stronger fill,
   never heavy */
#view-workflow .node-zoom-pill:hover,
#view-workflow .node-zoom-side-btn:hover {
  background: rgba(22, 22, 27, 0.74) !important;
  border-color: rgba(255, 255, 255, 0.16) !important;
}

/* ==============================================================================
   Workflow top-right controls (undo / redo / Save) — premium glass cluster
   ------------------------------------------------------------------------------
   The light-grey chunky icon squares + plain Save button floated over the dark
   canvas and read as a default UI kit. Rework them as one cohesive translucent-
   glass cluster that belongs to the premium dark canvas: undo/redo are quiet
   glass icon buttons; Save is the same material one step brighter so it stays
   the primary action. Class-based specificity — beats the base but yields to the
   more-specific light-canvas rule, so light mode keeps its own lighter treatment.
============================================================================== */
#view-workflow .wf-hist-btn {
  width: 32px; height: 32px; border-radius: 10px;
  background: rgba(20, 20, 25, 0.55);
  -webkit-backdrop-filter: blur(12px) saturate(130%);
  backdrop-filter: blur(12px) saturate(130%);
  border: 1px solid rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.6);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.05);
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
#view-workflow .wf-hist-btn:hover:not(:disabled) {
  background: rgba(42, 42, 50, 0.72);
  border-color: rgba(255, 255, 255, 0.16);
  color: rgba(255, 255, 255, 0.96);
}
#view-workflow .wf-hist-btn:active:not(:disabled) { transform: translateY(0.5px); }
#view-workflow .wf-hist-btn:disabled { opacity: 0.26; box-shadow: none; }
#view-workflow .wf-hist-btn .ph { width: 15px; height: 15px; }

#view-workflow .workflow-save-row .text-btn {
  height: 32px; padding: 0 18px; border-radius: 10px;
  background: rgba(255, 255, 255, 0.14);
  -webkit-backdrop-filter: blur(12px) saturate(130%);
  backdrop-filter: blur(12px) saturate(130%);
  border: 1px solid rgba(255, 255, 255, 0.22);
  color: rgba(255, 255, 255, 0.96);
  font-size: 0.78125rem; font-weight: 600; letter-spacing: 0.3px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.12);
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
#view-workflow .workflow-save-row .text-btn:hover {
  background: rgba(255, 255, 255, 0.22);
  border-color: rgba(255, 255, 255, 0.34);
}
#view-workflow .workflow-save-row .text-btn:active { transform: translateY(1px); }
/* Light-canvas mode: these premium glass controls would be light-on-light, so
   restore a solid light treatment (higher specificity than the dark rules). */
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .wf-hist-btn,
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .workflow-save-row .text-btn {
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(12, 14, 18, 0.16);
  color: #14171c;
  box-shadow: 0 2px 10px rgba(12, 18, 28, 0.14);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
#view-workflow:has(.node-canvas[data-canvas-lum="light"]) .wf-hist-btn:hover:not(:disabled) {
  background: #fff; border-color: rgba(12, 14, 18, 0.28); color: #14171c;
}

/* ==============================================================================
   TYPOGRAPHY SYSTEM — consolidated page-heading + label hierarchy
   ------------------------------------------------------------------------------
   Audit found the page title defined in ~8 scattered places (weight 600 vs 800,
   tracking -0.81px on tool pages vs +0.3px on Studio, drifting colours) and the
   subline set to Inter 300 — a weight the project never loads, so it rendered
   faux-thin and low-contrast. Micro labels ran 10.5px with 1.155px tracking
   (too small + over-tracked). This block is the single source of truth for the
   heading pair and the technical labels. Families are unchanged (Archivo display
   / Inter UI / IBM Plex Mono technical). Decision: page titles stay LEFT-aligned
   and read as one unit with their subline — a tool surface, not a landing page.

   Scale tokens (documentation + reuse):
     --text-title    700 28px/1.14  Archivo, -0.02em      page title, strongest
     --text-subtitle 400 13.5px/1.5 Inter                 page subline, muted
     --text-label    600 11px/1.4   Plex Mono, 0.6px, UC  technical section/panel
============================================================================== */
:root {
  --text-title-size: 28px;
  --text-subtitle-size: 13.5px;
  --text-label-track: 0.6px;
}

/* PAGE TITLE — strongest element, one consistent treatment on every view. */
.view .panel-head h1,
.panel-head h1 {
  font-family: var(--font-display) !important;
  font-size: var(--text-title-size) !important;
  font-weight: 700 !important;
  letter-spacing: -0.02em !important;
  line-height: 1.14 !important;
  text-transform: none !important;
  text-align: left !important;
  margin-bottom: 0 !important;
}

/* PAGE SUBLINE — one step down: readable weight (400, actually loaded), muted,
   set tight under the title so the pair reads as a single block. */
.view .panel-head .panel-sub,
.panel-head .panel-sub {
  font-family: var(--font-ui) !important;
  font-size: var(--text-subtitle-size) !important;
  font-weight: 400 !important;
  letter-spacing: 0 !important;
  line-height: 1.5 !important;
  text-align: left !important;
  margin: 7px 0 0 !important;
  max-width: 60ch;
}

/* TECHNICAL MICRO LABELS — keep the uppercase-mono character, but readable, not
   tiny, and with controlled (not excessive) tracking. Premium != tiny. */
.section-label {
  font-size: 0.6875rem !important;
  letter-spacing: var(--text-label-track) !important;
  font-weight: 600 !important;
}
.scene-group-title,
.scene-subgroup-title { letter-spacing: var(--text-label-track) !important; font-weight: 600 !important; }

/* New Project CTA — soften the bright-white block to a pale translucent surface
   that still reads as the primary action (not a solid white slab, not invisible).
   Same premium glass language as the Workflow Save button. */
.appbar-cta {
  background: rgba(255, 255, 255, 0.13);
  color: rgba(255, 255, 255, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.20);
  -webkit-backdrop-filter: blur(10px) saturate(130%);
  backdrop-filter: blur(10px) saturate(130%);
}
.appbar-cta:hover {
  background: rgba(255, 255, 255, 0.20);
  border-color: rgba(255, 255, 255, 0.32);
}

/* ==============================================================================
   Workflow node rail — the add-node menu, consolidated + spaced (Plumar-style)
   ------------------------------------------------------------------------------
   The add-node buttons were scattered across a cramped top quick-add row, a
   double-click popup, and the bottom dock. This is one calm left rail inside the
   canvas chrome: a node SEARCH + a spaced node list, in the same translucent
   dark glass as the tool dock, so it flows with the existing Workflow — not a
   new visual language. Existing createNode() does the work; nothing else moves.
============================================================================== */
#view-workflow .wf-node-rail {
  position: absolute; right: 22px; left: auto; top: 90px; width: 186px; z-index: 25;
  pointer-events: auto;
  background: none; border: 0; box-shadow: none; padding: 0;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  text-align: right;
}
.wf-nr-list { align-items: flex-end; }
.wf-nr-item { flex-direction: row-reverse; }
.wf-nr-search {
  display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 10px;
  border-radius: 9px; background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.07);
  margin-bottom: 8px;
}
/* stop the browser/password-manager from autofilling the node search */
.wf-nr-search input::-webkit-search-decoration,
.wf-nr-search input::-webkit-search-cancel-button,
.wf-nr-search input::-webkit-credentials-auto-fill-button { display: none !important; visibility: hidden; }
.wf-nr-search input { flex: 1; min-width: 0; background: none; border: 0; color: #f2f3f5; font-size: 0.78125rem; outline: none; font-family: var(--font-ui); }
.wf-nr-search input::placeholder { color: rgba(255, 255, 255, 0.36); }
.wf-nr-ic { color: rgba(255, 255, 255, 0.42); font-size: 0.8125rem; }
.wf-nr-kbd { font-family: var(--font-mono); font-size: 0.5625rem; color: rgba(255, 255, 255, 0.36); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 4px; padding: 1px 4px; }
.wf-nr-list { display: flex; flex-direction: column; gap: 2px; }
.wf-nr-item { display: flex; align-items: center; gap: 10px; padding: 8px 9px; border-radius: 8px; width: 100%; text-align: left; color: #f2f3f5; background: none; border: 0; cursor: pointer; transition: background 0.1s ease; }
.wf-nr-item:hover { background: rgba(255, 255, 255, 0.07); }
.wf-nr-i { width: 22px; height: 22px; border-radius: 6px; background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; color: #c8ccd4; flex: none; }
.wf-nr-t { font-size: 0.78125rem; font-weight: 500; }
.wf-nr-empty { padding: 10px; font-size: 0.71875rem; color: rgba(255, 255, 255, 0.36); }
/* The centred quick-add row is the empty-board way in; the has-nodes rules
   above hide it once the first node exists (the bottom dock takes over). */
@media (max-width: 860px) { #view-workflow .wf-node-rail { display: none; } }

/* ==============================================================================
   Workflow Characters drawer — reuse the existing Characters data as a handle
   ------------------------------------------------------------------------------
   Not a second character system: this lists the project's Characters (same
   /api/characters data), adds one to the board as a reference node, or opens
   its full Character Sheet page. Right side, under the node rail, near the Agent.
============================================================================== */
.wf-nr-cast { margin-top: 2px; }
#view-workflow .wf-cast-drawer {
  position: absolute; right: 22px; top: 296px; width: 224px; z-index: 26; pointer-events: auto;
  background: rgba(14,15,19,0.9);
  -webkit-backdrop-filter: blur(14px) saturate(130%); backdrop-filter: blur(14px) saturate(130%);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 14px; padding: 12px;
  box-shadow: 0 18px 44px rgba(0,0,0,0.5);
  max-height: calc(100vh - 340px); display: flex; flex-direction: column;
}
.wf-cast-drawer.hidden { display: none !important; }
.wf-cast-head { display: flex; align-items: center; justify-content: space-between; font-family: var(--font-display); font-weight: 700; font-size: 0.875rem; color: #f2f3f5; }
.wf-cast-x { width: 24px; height: 24px; border-radius: 7px; background: none; border: 0; color: rgba(255,255,255,0.6); cursor: pointer; font-size: 0.75rem; }
.wf-cast-x:hover { background: rgba(255,255,255,0.08); color: #f2f3f5; }
.wf-cast-hint { font-size: 0.6875rem; color: rgba(255,255,255,0.42); line-height: 1.5; margin: 6px 0 10px; }
.wf-cast-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; overflow-y: auto; }
.wf-cast-card { border-radius: 10px; overflow: hidden; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); }
.wf-cast-thumb { aspect-ratio: 1; background: linear-gradient(140deg,#3a3d44,#1a1c20); display: block; }
.wf-cast-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wf-cast-nm { font-size: 0.6875rem; font-weight: 600; color: #f2f3f5; padding: 6px 8px 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wf-cast-acts { display: flex; gap: 4px; padding: 2px 6px 7px; }
.wf-cast-acts button { flex: 1; font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.04em; text-transform: uppercase; padding: 4px 2px; border-radius: 6px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08); color: rgba(255,255,255,0.7); cursor: pointer; }
.wf-cast-acts button:hover { background: rgba(255,255,255,0.12); color: #f2f3f5; }
.wf-cast-empty { grid-column: 1 / -1; font-size: 0.71875rem; color: rgba(255,255,255,0.4); padding: 12px 4px; text-align: center; }
@media (max-width: 860px) { #view-workflow .wf-cast-drawer { display: none !important; } }

/* Workflow node-rail hover explainer — plain-English "what this node does" */
.wf-nr-pop { position: fixed; z-index: 60; width: 236px; border-radius: 12px; padding: 12px 14px;
  background: rgba(20,21,26,0.96); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border: 1px solid rgba(255,255,255,0.16); box-shadow: 0 18px 44px rgba(0,0,0,0.55);
  opacity: 0; pointer-events: none; transition: opacity 0.12s ease; color: #f2f3f5; }
.wf-nr-pop.show { opacity: 1; }
.wf-nr-pop .p-nm { font-family: var(--font-display); font-weight: 700; font-size: 0.9375rem; margin-bottom: 5px; }
.wf-nr-pop .p-what { font-size: 0.78125rem; line-height: 1.55; color: rgba(255,255,255,0.85); }
.wf-nr-pop .p-io { display: flex; gap: 8px; margin-top: 10px; }
.wf-nr-pop .p-io > div { flex: 1; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 6px 8px; }
.wf-nr-pop .p-io .k { font-family: var(--font-mono); font-size: 0.46875rem; letter-spacing: 0.1em; text-transform: uppercase; color: rgba(255,255,255,0.4); }
.wf-nr-pop .p-io .v { font-size: 0.6875rem; color: rgba(255,255,255,0.7); margin-top: 2px; line-height: 1.35; }
.wf-nr-pop .p-when { margin-top: 9px; font-size: 0.6875rem; color: rgba(255,255,255,0.7); line-height: 1.5; }
.wf-nr-pop .p-when b { color: #c8ccd4; }

/* Workflow Recipes drawer — starter graphs you can drop onto the board */
#view-workflow .wf-recipes-drawer {
  position: absolute; right: 22px; top: 296px; width: 236px; z-index: 26; pointer-events: auto;
  background: rgba(14,15,19,0.9);
  -webkit-backdrop-filter: blur(14px) saturate(130%); backdrop-filter: blur(14px) saturate(130%);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 14px; padding: 12px;
  box-shadow: 0 18px 44px rgba(0,0,0,0.5);
  max-height: calc(100vh - 340px); display: flex; flex-direction: column;
}
.wf-recipes-drawer.hidden { display: none !important; }
.wf-rec-save { display: flex; gap: 6px; margin-bottom: 10px; }
.wf-rec-save input { flex: 1; min-width: 0; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.09); border-radius: 8px; padding: 6px 9px; color: #f2f3f5; font-family: var(--font-ui, inherit); font-size: 0.71875rem; outline: none; }
.wf-rec-save input:focus { border-color: rgba(255,255,255,0.22); }
.wf-rec-save input::-webkit-credentials-auto-fill-button { display: none !important; visibility: hidden; }
.wf-rec-save button { font-family: var(--font-mono); font-size: 0.53125rem; letter-spacing: 0.05em; text-transform: uppercase; padding: 0 9px; border-radius: 8px; background: rgba(255,255,255,0.09); border: 1px solid rgba(255,255,255,0.12); color: #f2f3f5; cursor: pointer; white-space: nowrap; }
.wf-rec-save button:hover { background: rgba(255,255,255,0.16); }
.wf-rec-save button:disabled { opacity: 0.4; cursor: default; }
.wf-rec-list { display: flex; flex-direction: column; gap: 8px; overflow-y: auto; }
.wf-rec-item { border-radius: 10px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); padding: 9px 10px; }
.wf-rec-item.builtin { border-color: rgba(255,255,255,0.06); background: rgba(255,255,255,0.02); }
.wf-rec-nm { font-family: var(--font-display); font-weight: 700; font-size: 0.78125rem; color: #f2f3f5; display: flex; align-items: center; gap: 6px; }
.wf-rec-badge { font-family: var(--font-mono); font-size: 0.4375rem; letter-spacing: 0.08em; text-transform: uppercase; color: rgba(255,255,255,0.4); border: 1px solid rgba(255,255,255,0.12); border-radius: 4px; padding: 1px 4px; }
.wf-rec-desc { font-size: 0.65625rem; line-height: 1.45; color: rgba(255,255,255,0.5); margin: 4px 0 8px; }
.wf-rec-acts { display: flex; gap: 5px; }
.wf-rec-acts button { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.04em; text-transform: uppercase; padding: 4px 8px; border-radius: 6px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08); color: rgba(255,255,255,0.72); cursor: pointer; }
.wf-rec-acts .rec-load { flex: 1; }
.wf-rec-acts button:hover { background: rgba(255,255,255,0.13); color: #f2f3f5; }
.wf-rec-acts .rec-del:hover { background: rgba(255,80,80,0.18); border-color: rgba(255,120,120,0.35); color: #ffd9d9; }
.wf-rec-empty { font-size: 0.6875rem; color: rgba(255,255,255,0.4); padding: 10px 4px; text-align: center; }
@media (max-width: 860px) { #view-workflow .wf-recipes-drawer { display: none !important; } }

/* Contenteditable text boxes used for the node filter + recipe name.
   Not a real <input>, so no browser/password-manager credential dropdown can
   ever attach to it. Styled to look and behave like a single-line field. */
.wf-cedit {
  flex: 1; min-width: 0; background: none; border: 0; color: #f2f3f5;
  font-size: 0.78125rem; font-family: var(--font-ui); outline: none; line-height: 1.4;
  white-space: nowrap; overflow-x: auto; overflow-y: hidden; cursor: text;
  scrollbar-width: none; -ms-overflow-style: none;
}
.wf-cedit::-webkit-scrollbar { display: none; }
.wf-cedit:empty::before { content: attr(data-placeholder); color: rgba(255,255,255,0.36); pointer-events: none; }
.wf-rec-save .wf-cedit { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.09); border-radius: 8px; padding: 6px 9px; font-size: 0.71875rem; }
.wf-rec-save .wf-cedit:focus { border-color: rgba(255,255,255,0.22); }

/* Preset node quick-pick chips (Camera / Color / Style) */
.wf-preset-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 7px; }
.wf-preset-chip { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.03em; text-transform: uppercase; padding: 3px 6px; border-radius: 5px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.09); color: rgba(255,255,255,0.66); cursor: pointer; white-space: nowrap; }
.wf-preset-chip:hover { background: rgba(255,255,255,0.13); color: #f2f3f5; }
.wf-preset-ta { width: 100%; }

/* Script / Show Bible long-form text nodes */
.wf-doc-ta { width: 100%; min-height: 92px; }

/* Look-lock: pin star on cast cards + locked-look strip */
.wf-cast-card { position: relative; }
.ca-pin { position: absolute; top: 5px; right: 5px; width: 22px; height: 22px; border-radius: 6px; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.14); color: rgba(255,255,255,0.72); font-size: 0.75rem; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
.ca-pin:hover { background: rgba(0,0,0,0.7); color: #fff; }
.ca-pin.pinned { color: #ffffff; border-color: rgba(255,255,255,0.55); background: rgba(255,255,255,0.16); }
.wf-cast-lock { display: flex; align-items: center; gap: 8px; padding: 7px; margin-bottom: 10px; border-radius: 10px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.14); }
.wf-cast-lock.hidden { display: none !important; }
.wf-lock-thumb { width: 34px; height: 34px; border-radius: 7px; overflow: hidden; background: linear-gradient(140deg,#3a3d44,#1a1c20); flex: none; }
.wf-lock-thumb img { width: 100%; height: 100%; object-fit: cover; }
.wf-lock-info { flex: 1; min-width: 0; }
.wf-lock-k { font-family: var(--font-mono); font-size: 0.46875rem; letter-spacing: 0.1em; text-transform: uppercase; color: rgba(255,255,255,0.45); }
.wf-lock-nm { font-size: 0.71875rem; font-weight: 600; color: #f2f3f5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wf-lock-acts { display: flex; gap: 4px; align-items: center; }
.wf-lock-add { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.04em; text-transform: uppercase; padding: 5px 7px; border-radius: 6px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.12); color: #f2f3f5; cursor: pointer; white-space: nowrap; }
.wf-lock-add:hover { background: rgba(255,255,255,0.16); }
.wf-lock-x { width: 20px; height: 20px; border-radius: 5px; background: none; border: 0; color: rgba(255,255,255,0.5); cursor: pointer; font-size: 0.625rem; }
.wf-lock-x:hover { background: rgba(255,255,255,0.1); color: #f2f3f5; }

/* ─── Node palette folded into the bottom bar ───
   The rail is no longer pinned to the right edge (the agent lives there).
   It opens as a popover above the bottom dock, from the ⊞ button. */
#view-workflow .wf-node-rail {
  left: 50% !important; right: auto !important; top: auto !important;
  transform: translateX(-50%); bottom: 72px !important;
  width: 234px !important; z-index: 40 !important; text-align: left !important;
  background: rgba(14,15,19,0.94) !important;
  -webkit-backdrop-filter: blur(16px) saturate(140%); backdrop-filter: blur(16px) saturate(140%);
  border: 1px solid rgba(255,255,255,0.12) !important; border-radius: 14px !important;
  padding: 10px !important; box-shadow: 0 18px 50px rgba(0,0,0,0.6) !important;
  max-height: calc(100vh - 150px); overflow-y: auto;
}
#view-workflow .wf-node-rail.hidden { display: none !important; }
#view-workflow .wf-nr-list { align-items: stretch !important; }
#view-workflow .wf-nr-item { flex-direction: row !important; }
/* Characters / Recipes drawers also open above the dock, not on the right */
#view-workflow .wf-cast-drawer, #view-workflow .wf-recipes-drawer {
  left: 50% !important; right: auto !important; top: auto !important;
  transform: translateX(-50%); bottom: 72px !important;
  max-height: calc(100vh - 150px) !important;
}
#view-workflow .wf-tool-palette.active { color: #fff !important; background: rgba(255,255,255,0.16) !important; }

/* Note node — a sticky, not a generator: no ports, no generate, no merge */
#view-workflow .node.node-type-note .node-port { display: none !important; }
#view-workflow .node.node-type-note { border-left: 2px solid rgba(216,210,196,0.45); }
.wf-note-ta { width: 100%; min-height: 60px; }

/* ============================================================================
   STUDIO AGENT — WORKFLOW-ONLY DARK TRANSLUCENT VARIANT
   ----------------------------------------------------------------------------
   Applies ONLY while the Workflow node view is the active route, scoped with
   body:has(#view-workflow.active). Every other page keeps the shared LIGHT
   Studio Agent styling untouched, and the override reverts the instant you
   navigate away (:has re-evaluates when #view-workflow loses .active).
   VISUAL ONLY: no position/size/layout changes — the drawer's top boundary
   (top: var(--top-frame-height)) is preserved, no cap/spacer/padding hack.
   ONE component, ONE page-scoped variant (no duplicate Studio Agent).
   ============================================================================ */

/* Panel — near-black graphite, translucent, subtle border, very mild blur */
body:has(#view-workflow.active) .sd-drawer {
  background: rgba(12, 12, 15, 0.82) !important;
  border-left: 1px solid rgba(255, 255, 255, 0.10) !important;
  -webkit-backdrop-filter: blur(10px) saturate(118%); backdrop-filter: blur(10px) saturate(118%);
  box-shadow: -10px 0 30px rgba(0, 0, 0, 0.5) !important;
}
/* Heading + secondary text */
body:has(#view-workflow.active) .sd-title { color: #f2f3f5 !important; }
body:has(#view-workflow.active) .sd-subtitle { color: rgba(255, 255, 255, 0.5) !important; }
body:has(#view-workflow.active) .sd-thinking { color: rgba(255, 255, 255, 0.5) !important; }
/* Control chips (project selector, Chats, New Chat) */
body:has(#view-workflow.active) .sd-project-line,
body:has(#view-workflow.active) .sd-chip-btn {
  background: rgba(255, 255, 255, 0.05) !important;
  border: 1px solid rgba(255, 255, 255, 0.12) !important;
  color: rgba(255, 255, 255, 0.78) !important;
}
body:has(#view-workflow.active) .sd-project-line:hover,
body:has(#view-workflow.active) .sd-chip-btn:hover {
  background: rgba(255, 255, 255, 0.10) !important;
  border-color: rgba(255, 255, 255, 0.24) !important;
  color: #ffffff !important;
}
/* Dropdowns (project / chats popovers) so nothing pops out bright */
body:has(#view-workflow.active) .sd-popover {
  background: rgba(20, 20, 24, 0.97) !important;
  border: 1px solid rgba(255, 255, 255, 0.12) !important;
  color: rgba(255, 255, 255, 0.85) !important;
}
body:has(#view-workflow.active) .sd-popover-item { color: rgba(255, 255, 255, 0.8) !important; }
body:has(#view-workflow.active) .sd-popover-item:hover { background: rgba(255, 255, 255, 0.08) !important; color: #fff !important; }
body:has(#view-workflow.active) .sd-popover-item.active { color: #ffffff !important; }
/* Messages area — let the graphite panel show through */
body:has(#view-workflow.active) .sd-messages { background: transparent !important; }
/* User bubble — darker graphite, bright text */
body:has(#view-workflow.active) .sd-msg.user {
  background: rgba(18, 18, 22, 0.92) !important;
  color: #ffffff !important;
  border: 1px solid rgba(255, 255, 255, 0.08) !important;
}
/* Agent bubble — slightly lighter graphite, readable light text */
body:has(#view-workflow.active) .sd-msg.assistant {
  background: rgba(40, 42, 48, 0.90) !important;
  color: #eef1f3 !important;
  border: 1px solid rgba(255, 255, 255, 0.10) !important;
}
body:has(#view-workflow.active) .sd-msg.error {
  background: rgba(44, 32, 32, 0.90) !important;
  color: #f0d6d6 !important;
  border: 1px solid rgba(255, 120, 120, 0.22) !important;
}
/* Resolved @reference tags stay readable on the dark bubbles */
body:has(#view-workflow.active) .sd-msg.assistant mark,
body:has(#view-workflow.active) .sd-msg.error mark { color: #dfe4ea !important; }
/* Composer — dark translucent input, subtle border, light placeholder */
body:has(#view-workflow.active) .sd-input-row {
  background: rgba(255, 255, 255, 0.03) !important;
  border-top: 1px solid rgba(255, 255, 255, 0.08) !important;
}
body:has(#view-workflow.active) .sd-input-row input {
  background: rgba(255, 255, 255, 0.05) !important;
  border: 1px solid rgba(255, 255, 255, 0.12) !important;
  color: #f2f3f5 !important;
}
body:has(#view-workflow.active) .sd-input-row input::placeholder { color: rgba(255, 255, 255, 0.42) !important; }
body:has(#view-workflow.active) .sd-input-row input:focus { border-color: rgba(255, 255, 255, 0.28) !important; }
/* Attach button visible on dark */
body:has(#view-workflow.active) .sd-attach-btn {
  background: rgba(255, 255, 255, 0.06) !important;
  border: 1px solid rgba(255, 255, 255, 0.12) !important;
  color: rgba(255, 255, 255, 0.8) !important;
}
/* Send button — clearly visible, restrained premium */
body:has(#view-workflow.active) .sd-send-btn {
  background: rgba(255, 255, 255, 0.14) !important;
  color: #ffffff !important;
  border: 1px solid rgba(255, 255, 255, 0.20) !important;
}
body:has(#view-workflow.active) .sd-send-btn:hover { background: rgba(255, 255, 255, 0.22) !important; }

/* Workflow-only: the Studio Agent right-edge tab (open + closed states share
   .sd-rail-tab) matches the dark translucent panel. Scoped to the active
   Workflow view; reverts to the light tab on every other page. Position, size,
   and behavior are untouched — visual only. */
body:has(#view-workflow.active) .sd-rail-tab {
  background: rgba(12, 12, 15, 0.82) !important;
  border: 1px solid rgba(255, 255, 255, 0.10) !important;
  border-right: none !important;
  color: rgba(255, 255, 255, 0.82) !important;
  -webkit-backdrop-filter: blur(10px) saturate(118%); backdrop-filter: blur(10px) saturate(118%);
  box-shadow: -4px 0 14px rgba(0, 0, 0, 0.45) !important;
}
body:has(#view-workflow.active) .sd-rail-tab:hover {
  background: rgba(24, 24, 28, 0.9) !important;
  border-color: rgba(255, 255, 255, 0.22) !important;
  color: #ffffff !important;
  opacity: 1 !important;
}

/* ============================================================================
   WORKFLOW NODE — internal overlap fixes (header action group + port-safe body)
   ============================================================================ */
/* FIX A — header action group. The ✦ enhance and ↺ undo buttons were
   position:absolute (top:8px/right:10px & 36px) and landed on top of the
   flex-flow ✕ close button. Return them to the header's flex flow so
   title | [✦][↺][✕] each get their own space via the header's own gap. Only
   the else-branch head (prompt/preset/doc) uses .node-head > these buttons;
   Image/Video keep their separate toprow/box placement, untouched. */
#view-workflow .node-head > .node-enhance-btn,
#view-workflow .node-head > .node-enhance-undo {
  position: static !important;
  top: auto !important; right: auto !important;
  flex: 0 0 auto;
  margin: 0;
}
/* The undo control is hidden until an enhancement has actually run. The base
   .node-enhance-undo{display:flex} was overriding the [hidden] attribute, so a
   dead ↺ button showed in every header once it flowed inline — hide it while
   [hidden], matching the Image/Video toprow behavior. */
#view-workflow .node-head > .node-enhance-undo[hidden] { display: none !important; }
/* FIX B — port-safe body inset for preset/doc/note nodes. Their bodies were
   padding:0, so chip rows and text ran flush to the node edge and under the
   left/right connection ports (which reach ~10px inside the edge). Give them
   the same 11px side inset prompt/reference already use, and fold the
   textarea's own side padding into that inset so text and chips share one
   consistent safe gutter (no double gutter). */
#view-workflow .node-type-camera .node-body,
#view-workflow .node-type-cinematographer .node-body,
#view-workflow .node-type-colorgrade .node-body,
#view-workflow .node-type-style .node-body,
#view-workflow .node-type-script .node-body,
#view-workflow .node-type-bible .node-body,
#view-workflow .node-type-note .node-body {
  padding: 0 11px 11px;
}
#view-workflow .node-type-camera .wf-prompt,
#view-workflow .node-type-cinematographer .wf-prompt,
#view-workflow .node-type-colorgrade .wf-prompt,
#view-workflow .node-type-style .wf-prompt,
#view-workflow .node-type-script .wf-prompt,
#view-workflow .node-type-bible .wf-prompt,
#view-workflow .node-type-note .wf-prompt {
  padding-left: 0; padding-right: 0;
}

/* Node Cancel button — appears left of Generate only while a node is
   generating; cancels the job (and refunds credits, server-side). */
.node-cancel-btn {
  position: absolute; right: 12px; bottom: 12px;
  width: 30px; height: 30px; border-radius: 50%;
  background: rgba(20,20,24,0.9); color: rgba(255,255,255,0.85);
  border: 1px solid rgba(255,255,255,0.18); cursor: pointer; z-index: 5;
  display: flex; align-items: center; justify-content: center; font-size: 0.75rem;
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
.node-cancel-btn[hidden] { display: none !important; }
.node-cancel-btn:hover { background: rgba(180,50,50,0.92); border-color: rgba(255,120,120,0.5); color: #fff; }
.node-cancel-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Cancel sits IN the control row, on the same line as Generate — but to its
   LEFT, not its right. Generate is DOM-ordered before Cancel and carries
   margin-left:auto, so a naive static Cancel landed to the right of a
   right-pinned Generate and hung off the node's edge (clipped). Reorder so
   Generate is the rightmost item (flush to the padding edge) and Cancel takes
   the auto-margin + sits just inboard of it, so neither is ever cut off. */
/* The controls track (flex:1) now pushes the button cluster to the right on its
   own, so the buttons only need to be fixed-size and ordered: Cancel just left
   of a rightmost Generate. The 7px botrow gap spaces them. Neither can be
   clipped now — the controls give up width before the buttons do. */
#view-workflow .node .node-botrow .node-generate-btn { order: 3; flex: 0 0 auto; margin: 0; }
#view-workflow .node .node-botrow .node-cancel-btn {
  position: static;
  flex: 0 0 auto;
  order: 2;
  width: 30px; height: 30px; min-width: 30px;
  top: auto; right: auto; bottom: auto; left: auto;
  margin: 0;
}

/* ─── Empty-board hero — Workflow icon + copy above the quick-add buttons ─── */
#view-workflow .wf-quickadd-row {
  flex-direction: column;
  align-items: center;
  gap: 28px;
  transform: translate(-50%, -50%);
}
.wf-quickadd-btns { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 8px; }
.wf-empty-hero { display: flex; flex-direction: column; align-items: center; text-align: center; pointer-events: none; }
.wf-empty-icon {
  width: 92px; height: 92px; border-radius: 24px;
  background: rgba(255,255,255,0.045); border: 1px solid rgba(255,255,255,0.09);
  display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,0.62);
  box-shadow: 0 12px 34px rgba(0,0,0,0.4);
}
.wf-empty-icon svg { width: 44px; height: 44px; }
.wf-empty-title { font-family: var(--font-display, 'Archivo', sans-serif); font-weight: 700; font-size: 1.3125rem; letter-spacing: -0.01em; color: #f2f3f5; margin-top: 20px; }
.wf-empty-sub { font-size: 0.8125rem; line-height: 1.55; color: rgba(255,255,255,0.5); margin-top: 9px; max-width: 330px; }
/* the older one-line hint is superseded by the hero copy on an empty board */
#view-workflow .node-canvas:not(.has-nodes) .node-canvas-hint { display: none; }

/* Rubber-band multi-select box */
.wf-marquee { position: fixed; z-index: 9999; pointer-events: none;
  border: 1px solid rgba(255,255,255,0.55); background: rgba(255,255,255,0.08); border-radius: 3px; }

/* ============================================================================
   PREMIUM NODE CONTROLS (high-end design pass)
   Haptic depth (hairline + inner highlight + gradient fill), spring-eased
   hover lift and press-scale, on-brand silver/graphite — no colour.
   Scoped to Workflow so no other page's controls change.
   ============================================================================ */
/* Quick-pick preset chips (Camera / Cinematographer / Color / Style) */
#view-workflow .wf-preset-chip {
  font-family: var(--font-mono);
  font-size: 0.53125rem; letter-spacing: 0.07em; text-transform: uppercase;
  padding: 4px 9px; border-radius: 7px; white-space: nowrap; cursor: pointer;
  color: rgba(255,255,255,0.6);
  background: linear-gradient(180deg, rgba(255,255,255,0.065), rgba(255,255,255,0.022));
  border: 1px solid rgba(255,255,255,0.09);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.07), 0 1px 2px rgba(0,0,0,0.28);
  transition: transform 0.22s cubic-bezier(0.32,0.72,0,1), background 0.2s ease, border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
#view-workflow .wf-preset-chip:hover {
  color: #f4f5f7;
  background: linear-gradient(180deg, rgba(255,255,255,0.13), rgba(255,255,255,0.05));
  border-color: rgba(255,255,255,0.22);
  transform: translateY(-1px);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.12), 0 4px 11px rgba(0,0,0,0.42);
}
#view-workflow .wf-preset-chip:active { transform: translateY(0) scale(0.96); box-shadow: inset 0 1px 2px rgba(0,0,0,0.32); }

/* Round header action buttons — enhance ✦ / undo ↺ / close ✕ (machined) */
#view-workflow .node-head > .node-enhance-btn,
#view-workflow .node-head > .node-enhance-undo,
#view-workflow .node-close {
  background: linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.035)) !important;
  border: 1px solid rgba(255,255,255,0.13) !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.11), 0 1px 3px rgba(0,0,0,0.32);
  color: var(--wf-ink-mid);
  transition: transform 0.22s cubic-bezier(0.32,0.72,0,1), background 0.2s ease, border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
#view-workflow .node-head > .node-enhance-btn:hover,
#view-workflow .node-head > .node-enhance-undo:hover,
#view-workflow .node-close:hover {
  color: #fff;
  background: linear-gradient(180deg, rgba(255,255,255,0.17), rgba(255,255,255,0.07)) !important;
  border-color: rgba(255,255,255,0.28) !important;
  transform: translateY(-1px);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.17), 0 4px 11px rgba(0,0,0,0.42) !important;
}
#view-workflow .node-head > .node-enhance-btn:active,
#view-workflow .node-head > .node-enhance-undo:active,
#view-workflow .node-close:active { transform: scale(0.9); }

/* Generate ↑ — brushed-silver pill, for consistency across the node family */
#view-workflow .node-generate-btn {
  background: linear-gradient(180deg, #f5f6f8, #d6dae0) !important;
  color: #14151a !important; border: 0 !important;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.75), 0 2px 8px rgba(0,0,0,0.42) !important;
  transition: transform 0.22s cubic-bezier(0.32,0.72,0,1), filter 0.2s ease, box-shadow 0.2s ease;
}
#view-workflow .node-generate-btn:hover:not(:disabled) {
  transform: translateY(-1px); filter: brightness(1.05);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.85), 0 6px 16px rgba(0,0,0,0.5) !important;
}
#view-workflow .node-generate-btn:active:not(:disabled) { transform: scale(0.93); }

/* Coverage node — shot picker + build */
.wf-cov-eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.14em; text-transform: uppercase; color: rgba(255,255,255,0.4); margin-bottom: 9px; }
.wf-cov-shots { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 5px; margin-bottom: 9px; }
.wf-cov-chip {
  font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 7px 4px; border-radius: 8px; cursor: pointer; color: rgba(255,255,255,0.5);
  background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.018));
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
  transition: transform 0.2s cubic-bezier(0.32,0.72,0,1), background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.wf-cov-chip:hover { color: #eef1f3; border-color: rgba(255,255,255,0.18); transform: translateY(-1px); }
.wf-cov-chip.on {
  color: #14151a; background: linear-gradient(180deg, #f4f5f7, #d8dce1);
  border-color: rgba(255,255,255,0.5); box-shadow: inset 0 1px 0 rgba(255,255,255,0.7), 0 2px 6px rgba(0,0,0,0.35);
}
.wf-cov-ta { width: 100%; }
.wf-cov-build {
  margin-top: 9px; width: 100%; padding: 9px; border-radius: 10px; cursor: pointer;
  font-family: var(--font-mono); font-size: 0.59375rem; letter-spacing: 0.08em; text-transform: uppercase; color: #f2f3f5;
  background: linear-gradient(180deg, rgba(255,255,255,0.11), rgba(255,255,255,0.045));
  border: 1px solid rgba(255,255,255,0.15);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.1), 0 1px 3px rgba(0,0,0,0.3);
  transition: transform 0.2s cubic-bezier(0.32,0.72,0,1), background 0.2s ease, border-color 0.2s ease;
}
.wf-cov-build:hover { background: linear-gradient(180deg, rgba(255,255,255,0.17), rgba(255,255,255,0.07)); border-color: rgba(255,255,255,0.28); transform: translateY(-1px); }
.wf-cov-build:active { transform: scale(0.98); }

/* Corner resize grips */
.node-resize-handle { position: absolute; width: 18px; height: 18px; z-index: 9; opacity: 0; transition: opacity 0.12s ease; }
#view-workflow .node:hover .node-resize-handle, #view-workflow .node.selected .node-resize-handle { opacity: 1; }
.node-resize-handle::after { content: ''; position: absolute; inset: 6px; border-radius: 50%; background: rgba(255,255,255,0.6); border: 1px solid rgba(0,0,0,0.5); box-shadow: 0 1px 2px rgba(0,0,0,0.45); }
.rh-nw { top: -7px; left: -7px; cursor: nwse-resize; }
.rh-ne { top: -7px; right: -7px; cursor: nesw-resize; }
.rh-sw { bottom: -7px; left: -7px; cursor: nesw-resize; }
.rh-se { bottom: -7px; right: -7px; cursor: nwse-resize; }

/* Group outline box (behind the grouped nodes) */
.wf-group-box { position: absolute; z-index: 1; pointer-events: none;
  border: 1px dashed rgba(255,255,255,0.22); border-radius: 20px; background: rgba(255,255,255,0.018); }

/* Coverage node — compact perspectives grid (checkbox options) */
.node-type-coverage { width: 320px; }
#view-workflow .node-type-coverage .node-body { padding: 4px 12px 12px; }
.wf-cov-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 9px; }
.wf-cov-head .wf-cov-eyebrow { margin-bottom: 0; }
.wf-cov-count { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.06em; color: rgba(255,255,255,0.42); }
.wf-cov-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 8px; margin-bottom: 11px; }
.wf-cov-opt {
  display: flex; align-items: center; gap: 8px; padding: 5px 6px; border-radius: 8px;
  background: none; border: 0; cursor: pointer; text-align: left; width: 100%;
  transition: background 0.15s ease;
}
.wf-cov-opt:hover { background: rgba(255,255,255,0.05); }
.wf-cov-check {
  width: 15px; height: 15px; border-radius: 5px; flex: none; position: relative;
  border: 1.5px solid rgba(255,255,255,0.24);
  background: rgba(255,255,255,0.02);
  transition: background 0.18s cubic-bezier(0.32,0.72,0,1), border-color 0.18s ease, transform 0.18s cubic-bezier(0.32,0.72,0,1);
}
.wf-cov-opt:hover .wf-cov-check { border-color: rgba(255,255,255,0.4); }
.wf-cov-opt.on .wf-cov-check {
  background: linear-gradient(180deg, #f5f6f8, #d7dbe0); border-color: transparent;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.7), 0 1px 3px rgba(0,0,0,0.35);
}
.wf-cov-opt.on .wf-cov-check::after {
  content: '✓'; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: #14151a; font-size: 0.625rem; font-weight: 800; line-height: 1;
}
.wf-cov-lbl { font-size: 0.65625rem; font-weight: 500; color: rgba(255,255,255,0.5); white-space: nowrap; transition: color 0.15s ease; }
.wf-cov-opt.on .wf-cov-lbl { color: #f2f3f5; }

/* Create-node palette can now hold the full applicable list — cap + scroll */
#view-workflow .node-palette { max-height: min(60vh, 420px); overflow-y: auto; }

/* ─── IMAGE FILLS THE WHOLE NODE ───────────────────────────────────────────
   The media was inset 44px top and bottom so the scrim rows had a clear band,
   which left two dark strips of the card showing above and below the picture.
   The toprow/botrow are gradient scrims for exactly this reason — they stay
   readable straight over the frame — so the media goes full-bleed edge to edge
   and the controls float on top of it. Scoped to image/video so preset/prompt/
   doc nodes (which carry a body, not a picture) are untouched. */
#view-workflow .node-type-image .node-box > .node-box-inner,
#view-workflow .node-type-video .node-box > .node-box-inner {
  top: 0; bottom: 0;
}

/* ─── NODE IMAGES MUST NOT START A NATIVE DRAG ─────────────────────────────
   Browser <img>/<video> are draggable by default, so pressing on a node's
   picture to MOVE the node instead kicked off a native image drag-and-drop
   (the translucent ghost). On an all-image Reference/Character node the whole
   surface is the picture, so it couldn't be moved or corner-resized at all —
   the gesture was always eaten by the image DnD. Killing user-drag hands the
   press back to the node's own pointer drag/resize handlers. */
#view-workflow .node img,
#view-workflow .node video {
  -webkit-user-drag: none;
  user-select: none;
}

/* ═══════════════ COLOR GRADE — LOOK PICKER WITH REAL SWATCHES ══════════════
   The Color node is a categorised look library, not a text box: it shows the
   selected grade's 6-swatch palette + a film reference, and opens a full panel
   of looks. Palette-first so the choice is visual. */
#view-workflow .node-type-colorgrade { width: 300px; }
#view-workflow .node-type-colorgrade .node-body { padding: 10px 13px 12px; }
.wf-cg-eyebrow {
  font-size: 0.71875rem; color: rgba(255,255,255,0.42); line-height: 1.4;
  margin-bottom: 10px; cursor: pointer;
}
.wf-cg-selected { cursor: pointer; }
.wf-cg-swatches, .wf-cg-rowsw {
  display: flex; width: 100%; height: 26px; border-radius: 6px; overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08);
}
.wf-cg-swatches span, .wf-cg-rowsw span { flex: 1 1 0; display: block; }
.wf-cg-emptysw { opacity: 0.28; }
.wf-cg-emptysw span { background: repeating-linear-gradient(45deg, rgba(255,255,255,0.06) 0 4px, rgba(255,255,255,0.02) 4px 8px); }
.wf-cg-name {
  font-family: var(--font-display, 'Archivo', sans-serif); font-weight: 600;
  font-size: 0.9375rem; letter-spacing: -0.01em; color: #f2f3f5; margin-top: 10px;
}
.wf-cg-name.wf-cg-muted { color: rgba(255,255,255,0.55); font-weight: 500; }
.wf-cg-desc { font-size: 0.75rem; font-style: italic; color: rgba(255,255,255,0.5); line-height: 1.45; margin-top: 4px; }
.wf-cg-browse {
  margin-top: 12px; width: 100%; height: 32px; border-radius: 8px;
  background: linear-gradient(180deg, rgba(255,255,255,0.09), rgba(255,255,255,0.04));
  border: 1px solid rgba(255,255,255,0.12); color: rgba(255,255,255,0.9);
  font-size: 0.78125rem; font-weight: 600; letter-spacing: 0.01em; cursor: pointer;
  transition: background 0.18s cubic-bezier(0.32,0.72,0,1), border-color 0.18s, transform 0.12s;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.08);
}
.wf-cg-browse:hover { background: linear-gradient(180deg, rgba(255,255,255,0.14), rgba(255,255,255,0.07)); border-color: rgba(255,255,255,0.22); }
.wf-cg-browse:active { transform: scale(0.98); }

/* ── The look panel (modal over the whole app) ── */
.wf-cg-panel-overlay {
  position: fixed; inset: 0; z-index: 10060;
  background: rgba(6,7,10,0.72); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.wf-cg-panel-card {
  width: min(560px, 96vw); max-height: 82vh; display: flex; flex-direction: column;
  background: linear-gradient(180deg, #14161c, #0e1014);
  border: 1px solid rgba(255,255,255,0.1); border-radius: 18px;
  box-shadow: 0 30px 80px -20px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.05);
  padding: 20px 20px 8px;
}
.wf-cg-panel-head { display: flex; align-items: center; justify-content: space-between; }
.wf-cg-panel-eyebrow { font-family: var(--font-mono,'IBM Plex Mono',monospace); font-size: 0.65625rem; letter-spacing: 0.18em; text-transform: uppercase; color: rgba(180,150,255,0.75); }
.wf-cg-panel-x {
  width: 30px; height: 30px; border-radius: 8px; background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.7); cursor: pointer; font-size: 0.8125rem;
}
.wf-cg-panel-x:hover { background: rgba(255,255,255,0.1); color: #fff; }
.wf-cg-panel-title { font-family: var(--font-display,'Archivo',sans-serif); font-weight: 700; font-size: 1.3125rem; color: #f2f3f5; margin: 6px 0 14px; }
.wf-cg-panel-list { overflow-y: auto; margin: 0 -6px; padding: 0 6px 12px; }
.wf-cg-cat { font-family: var(--font-mono,'IBM Plex Mono',monospace); font-size: 0.625rem; letter-spacing: 0.16em; text-transform: uppercase; color: rgba(255,255,255,0.4); margin: 16px 4px 8px; }
.wf-cg-cat:first-child { margin-top: 2px; }
.wf-cg-row {
  display: flex; align-items: center; gap: 14px; width: 100%; text-align: left;
  padding: 11px 12px; margin-bottom: 8px; border-radius: 12px;
  background: rgba(255,255,255,0.025); border: 1px solid rgba(255,255,255,0.07); cursor: pointer;
  transition: background 0.16s cubic-bezier(0.32,0.72,0,1), border-color 0.16s, transform 0.1s;
}
.wf-cg-row:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.16); }
.wf-cg-row:active { transform: scale(0.995); }
.wf-cg-row.on { border-color: rgba(180,150,255,0.5); background: rgba(120,90,200,0.12); }
.wf-cg-rowsw { width: 76px; min-width: 76px; height: 40px; border-radius: 8px; }
.wf-cg-rowtxt { min-width: 0; }
.wf-cg-rownm { font-size: 0.875rem; font-weight: 600; color: #f2f3f5; }
.wf-cg-rowdesc { font-size: 0.75rem; color: rgba(255,255,255,0.5); line-height: 1.4; margin-top: 2px; }

/* ═══════════ SHOTBORN STARTUP WORD-PRELOADER ═══════════
   Native to ShotBorn's system: near-black ground, silver display type,
   restrained fade (no gradients, no neon, no zoom). Shown only on a fresh app
   entry (see the inline gate in index.html), then fades to Studio. */
#cl-preloader {
  position: fixed; inset: 0; z-index: 100000;
  background: #050506;
  display: flex; align-items: center; justify-content: center;
  transition: opacity 0.46s cubic-bezier(0.32, 0.72, 0, 1);
}
#cl-preloader.cl-pre-out { opacity: 0; pointer-events: none; }
#cl-preloader .cl-pre-word {
  font-family: var(--font-display, 'Archivo', sans-serif);
  font-weight: 800;
  font-size: clamp(24px, 5.4vw, 56px);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #c9cace;                 /* silver / near-white */
  opacity: 0;
  text-align: center;
  max-width: 92vw;                /* multi-word closing line never overflows */
  padding: 0 18px;
  text-wrap: balance;
  text-shadow: 0 1px 30px rgba(255, 255, 255, 0.06);
  will-change: opacity, transform;
}
#cl-preloader .cl-pre-word.cl-in {
  animation: clPreWord 0.26s cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
@keyframes clPreWord {
  0%   { opacity: 0; transform: translateY(7px); }
  45%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  #cl-preloader .cl-pre-word.cl-in { animation: none; opacity: 1; transform: none; }
  #cl-preloader { transition: none; }
}

/* ── Studio hub header — centred title card (it's the app's home/landing) ──
   Scoped to studio-hub ONLY: every workspace page keeps its left-aligned
   title. Centres the title + subline on the grid's vertical axis and lifts the
   title size a step, restrained (no hero). */
#view-studio-hub .panel-head {
  text-align: center;
  padding: 46px 24px 26px;
}
@media (max-width: 640px) {
  #view-studio-hub .panel-head { padding: 26px 16px 20px; }
}
#view-studio-hub .panel-head h1 {
  /* !important + an id selector to win over the app-wide "one consistent page
     title" rule (which pins every h1 to --text-title-size AND text-align:left,
     both !important). Home is the deliberate centred exception. */
  font-size: clamp(34px, 4vw, 44px) !important;
  text-align: center !important;
  line-height: 1.1;
}
#view-studio-hub .panel-head .panel-sub {
  font-size: 0.9375rem;
  text-align: center !important;
  /* the subline has a max-width; auto side margins centre the box itself so it
     sits under the centred title instead of pinned left. */
  margin: 8px auto 0 !important;
}
/* The home hub has nothing to go "back" to — hide its vestigial mobile back. */
#view-studio-hub .panel-head .mobile-back-btn { display: none; }
/* Centre the header's underline so it matches the now-centred title. */
#view-studio-hub .panel-head::after { left: 50%; right: auto; transform: translateX(-50%); width: min(520px, 70%); background: linear-gradient(90deg, transparent, var(--border-soft) 50%, transparent); }

/* ═══════════ MOBILE: collapse the desktop rail/chrome clearance ═══════════
   On phones the left "rail" is a hamburger overlay, not a fixed sidebar, so
   the desktop 96px LEFT rail-clearance (and the redundant 78px view TOP
   clearance — main already reserves the fixed app-bar height on mobile) shoved
   every page ~96px right and well down the screen. These reuse the exact same
   selectors as the desktop clearance rules so, being later in source at equal
   specificity, they win under the mobile breakpoint only. */
@media (max-width: 860px) {
  #app-shell main > .view.active:not(#view-home):not(#view-workflow) {
    padding-left: 16px;
    padding-right: 16px;
    padding-top: 12px;
  }
  main:has(> #view-projects.active),
  main:has(> #view-explore.active) {
    padding: 12px 16px 44px;
  }
  :is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
      #view-shot-extractor, #view-remaster, #view-video, #view-avatar).active {
    padding-left: 16px;
    padding-right: 16px;
    padding-top: 12px;
  }
}

/* ── MOBILE: one consistent 16px left gutter for every page's Back/header ──
   Explore/Projects/Vault were double-gutted (main 16px + the view's own 16px =
   32px) while the creative views sat at 16px. Zero main's gutter for those so
   the view's single 16px gutter wins, matching every other page. */
@media (max-width: 860px) {
  main:has(> #view-explore.active),
  main:has(> #view-projects.active),
  main:has(> #view-vault.active) {
    padding-left: 0;
    padding-right: 0;
  }
  /* Workflow breaks out of main with a header nudged 78px right for the desktop
     floating pills; on mobile those pills aren't at the top-left, so pull its
     header back to the 16px gutter too. */
  #view-workflow.active .panel-head.panel-head-row { margin-left: 0; padding-left: 16px; }
}

/* ── MOBILE: align the Prompt Vault's content + category pills to the gutter ──
   The vault used a 36px content inset, and its category strip was pushed right
   by the </> scroll arrows + nav padding, so title/pills didn't line up with
   the Back button and search. On mobile: 16px content inset, drop the arrows
   (swipe scrolls the strip on touch), and zero the strip's left pad so the
   first pill starts at the same 16px gutter as everything else. */
@media (max-width: 860px) {
  #view-vault .content { padding-left: 16px; padding-right: 16px; }
  #view-vault .vault-catnav { display: none; }
  #view-vault .nav-scroll { padding-left: 0; padding-right: 14px; }
}

/* MOBILE: the vault topbar carried a 16px inset and the search/canvas a
   -16px compensating margin, so the "All Frames" title sat at 32px while
   everything else was at 16px. Zero both on mobile so the title, search and
   cards share the same 16px gutter as the Back button and category pills. */
@media (max-width: 860px) {
  #view-vault .topbar { padding-left: 0; padding-right: 0; }
  #view-vault .vault-canvas { margin-left: 0; margin-right: 0; }
  #view-vault .topbar .search-input,
  #view-vault .topbar .search { margin-left: 0; }
}

/* ── Remove the ○ port/bullet ring before every section label and field label ──
   Requested removed app-wide. This overrides the hollow-ring ::before defined
   earlier (the "unconnected port" motif) for all the creative views — same
   selector, later in the cascade, so display:none wins without !important. */
:is(#view-image, #view-characters, #view-charsheet, #view-scene, #view-storyboard,
    #view-shot-extractor, #view-remaster, #view-video, #view-avatar)
  :is(.section-label, .field > label)::before {
  display: none;
}

/* ── Text-to-Video structured prompt worksheet (right flyout, Text mode only) ──
   Eight optional descriptive fields stacked vertically. Reuses .section-label /
   .section-hint / .text-input; only the compact per-row rhythm is new (no
   dividers between rows — the single .t2v-field keeps them in one clean group,
   with the flyout's automatic divider separating them from the Prompt tips). */
.t2v-fieldrow + .t2v-fieldrow { margin-top: 16px; }
.t2v-fieldrow .section-hint { margin: 3px 0 7px; }

/* Desktop only: keep the flyout the same height as the control column and let
   the builder scroll INSIDE it rather than growing the panel. The inner wrapper
   is taken out of flow (absolute) so it never drives the grid row height — that
   stays defined by the control column — so the panel keeps its exact size.
   Below 1100px the grid stacks single-column (no sibling to define a height),
   so the wrapper returns to normal flow and the page scrolls as usual. */
@media (min-width: 1101px) {
  #video-flyout-panel.t2v { position: relative; padding: 0; }
  #video-flyout-panel.t2v .flyout-inner { position: absolute; inset: 0; overflow-y: auto; padding: 26px; }
}

/* ── Workflow phone-portrait gate ───────────────────────────────────────────
   Blocks the Workflow canvas on a portrait phone-sized viewport and shows a
   full-screen rotate/desktop notice. Pure CSS media query, so it appears and
   clears LIVE on rotation with no reload. Scoped to #view-workflow (which is
   display:none unless active) so it never shows on any other page or on
   desktop. Reuses the existing Workflow (FlowArrow) icon and the empty-state
   icon/title/sub treatment — no new visual language. Threshold: portrait AND
   max-width 600px, so phones (~360–430px portrait) match while tablets
   (≥600px, e.g. iPad at 768px) never do. */
.wf-portrait-gate { display: none; }
.wf-portrait-gate-inner {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 18px; max-width: 340px;
}
.wf-portrait-gate-icon {
  width: 92px; height: 92px; border-radius: 24px; flex: none;
  background: rgba(255,255,255,0.045); border: 1px solid rgba(255,255,255,0.09);
  display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,0.62);
  box-shadow: 0 12px 34px rgba(0,0,0,0.4);
}
.wf-portrait-gate-icon svg { width: 46px; height: 46px; }
.wf-portrait-gate-title {
  font-family: var(--font-display, 'Archivo', sans-serif);
  font-weight: 700; font-size: 1.25rem; letter-spacing: 0.02em; text-transform: uppercase;
  color: #f2f3f5; margin: 0;
}
.wf-portrait-gate-text { font-size: 0.8125rem; line-height: 1.6; color: rgba(255,255,255,0.5); margin: 0; max-width: 300px; }
.wf-portrait-gate-rotate { color: rgba(255,255,255,0.3); margin-top: 2px; }
.wf-portrait-gate-rotate svg { width: 118px; height: 40px; display: block; }
/* The gate swallows touches for the canvas behind it, but this control stays
   tappable so the user can leave without needing the rail underneath. */
.wf-portrait-gate-btn {
  margin-top: 6px; pointer-events: auto;
  padding: 11px 22px; border-radius: 999px; border: 0; cursor: pointer;
  background: #f2f3f5; color: #0a0a0c;
  font-family: var(--font-body); font-size: 0.8125rem; font-weight: 600;
  box-shadow: 0 6px 18px rgba(0,0,0,0.4);
}
.wf-portrait-gate-btn:active { transform: translateY(1px); }

/* Disable the node canvas on any TOUCH device (phones + tablets, every
   orientation) — a node editor needs a large screen and a precise pointer.
   `pointer: coarse` is the primary-input test: it matches phones/tablets and
   never a mouse/trackpad desktop (which stays fully enabled), and it resolves
   live with no JS or reload. Broadened from the old portrait-phone-only query
   per product decision that the small canvas isn't worth shipping on mobile. */
@media (pointer: coarse) {
  #view-workflow .wf-portrait-gate {
    display: flex; align-items: center; justify-content: center;
    position: fixed; inset: 0; z-index: 10070;
    padding: 40px 28px;
    background: #0a0a0c;
  }
  /* Belt-and-suspenders: the canvas beneath can't receive touches even if the
     fixed overlay were somehow bypassed. */
  #view-workflow .node-canvas-wrap { pointer-events: none; }
}

/* ── Workflow portrait gate: keep the menu button reachable ──────────────────
   The gate is a full takeover, but the collapsed-menu (hamburger) button stays
   above it so the user can open the nav and leave without rotating — NOT the
   whole app bar (logo, beta, account, etc. are hidden here). Scoped with :has()
   to the Workflow page in portrait-phone only, so no other page/orientation is
   affected, and the drawer it opens is raised above the gate too. */
@media (orientation: portrait) and (max-width: 600px) {
  #app-shell:has(#view-workflow.active) .appbar {
    z-index: 10080; background: transparent; border-bottom: 0; pointer-events: none;
  }
  #app-shell:has(#view-workflow.active) .appbar .appbar-right,
  #app-shell:has(#view-workflow.active) .appbar .appbar-logo,
  #app-shell:has(#view-workflow.active) .appbar .rail-beta-badge { display: none; }
  #app-shell:has(#view-workflow.active) .appbar #rail-toggle { pointer-events: auto; }
  /* The rail is a grid item with z-index:1, which would cap its drawer below the
     gate — raise the rail (and its panel) above the gate so the opened menu
     shows on top and the user can tap through to another page. */
  #app-shell:has(#view-workflow.active) .rail { z-index: 10086 !important; }
  #app-shell:has(#view-workflow.active) .rail-panel { z-index: 10086; }
}

/* ── Mobile / tablet LANDSCAPE: compact the centered top menu ─────────────────
   In portrait the app bar uses space-between (groups pinned to both edges),
   which in landscape stretches the shining-border notch across the whole width
   AND pins the right cluster (account + New Project) into the workflow Save
   row. In landscape below desktop width, keep the menu a tidy CENTERED capsule
   (like desktop) and compact the cluster so it fits cleanly, leaves the Save /
   undo / redo their own space on the right, and the JS-drawn notch (which
   redraws on resize from the live menu geometry) tightens with it. Desktop
   (wider than 1200px) is untouched; portrait is untouched. */
@media (orientation: landscape) and (max-width: 1200px) {
  .appbar { justify-content: center !important; gap: 0 !important; }
  .appbar-right { gap: 4px; }
  .appbar-icon-btn { width: 30px; height: 30px; }
  .appbar-account { max-width: 150px; gap: 6px; padding: 0 7px 0 4px; }
  .appbar-cta { padding: 0 10px; font-size: 0.78125rem; height: 34px; }
}

/* Very small landscape (small phones ~≤760px): the centered capsule + the
   workflow Save/undo/redo can't both fit, so drop the least-essential chrome
   from the menu — the BETA badge and the two secondary icons (canvas tone,
   Contact) — keeping Menu, Settings, account and New Project. Clears the Save
   cluster; larger phones/tablets keep the full set. */
@media (orientation: landscape) and (max-width: 760px) {
  .rail-beta-badge { display: none; }
  #appbar-tone, #contact-rail-btn { display: none; }
  .appbar-cta { padding: 0 9px; }
}

/* Portrait credits + identity. The ≤860px rules collapse the account block to
   just the avatar, which also hides the credit balance. In landscape the bar is
   wider than 860px so the balance stays visible — restore it in portrait, and
   show the username too wherever it fits (see the tiers below). */
@media (orientation: portrait) and (max-width: 860px) {
  .appbar-account .account-meta { display: flex; }
  .appbar-account .account-sub { display: none; }
  .appbar-account .account-name { display: none; }   /* shown in the fitting tier below */
  .appbar-account .rail-account-credits { display: flex; }
  .appbar-account .rail-account-credits[hidden] { display: none; }
}
/* Phones ≥388px (iPhone 12–16 / Plus / Pro Max and portrait tablets): the full
   bar fits with tightened spacing — username + credit balance + every role icon
   (Settings / Pricing / Admin, or Contact for subscribers) + the full New
   Project button. Only the decorative BETA badge is dropped. */
@media (orientation: portrait) and (min-width: 388px) and (max-width: 860px) {
  .rail-beta-badge { display: none; }
  .appbar-account .account-name { display: block; max-width: 96px; }
  .appbar-right { gap: 4px; }
  .appbar-icon-btn { width: 28px; height: 28px; }
  .appbar-account { gap: 5px; padding: 0 5px 0 3px; }
  .appbar-cta { padding: 0 9px; }
}
/* Small phones ≤387px (SE / mini / compact Android): not enough width for the
   username + full label, so drop the username and collapse New Project to its
   "+" glyph — the credit balance and every role icon still show. */
@media (orientation: portrait) and (max-width: 387px) {
  .rail-beta-badge { display: none; }
  .appbar-cta .appbar-cta-label { display: none; }
  .appbar-cta { padding: 0 9px; }
  .appbar-right { gap: 4px; }
}

/* On the Studio home the rail is hidden (the section cards are the nav there),
   but on mobile the top-bar menu button still needs to open it as a drawer —
   without this it toggled a display:none rail and nothing appeared. The extra
   .open selector outranks the hide, so the drawer shows only when opened. */
@media (max-width: 860px) {
  #app-shell:has(#view-studio-hub.active) .rail.open { display: block !important; }
}

/* ── Live Prompt box (Text-to-Video, left column) ────────────────────────────
   The READ-ONLY assembled output of the main prompt + the optional right-side
   guidance fields. Deliberately distinct from the editable main prompt: a
   recessed inset surface in mono type with a subtle LIVE badge, so it reads as
   output, never a second input. Reuses .section-label-row / .section-label and
   existing tokens; hidden in every non-Text mode via .hidden (JS toggle). */
.t2v-live-badge {
  font-family: var(--font-mono);
  font-size: 0.5625rem; font-weight: 700; letter-spacing: 1.1px;
  color: var(--text-faint);
  border: 1px solid var(--border-soft);
  border-radius: 4px; padding: 2px 7px; line-height: 1;
  display: inline-flex; align-items: center; gap: 5px;
}
.t2v-live-badge::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--text-muted); flex: none;
}
.t2v-liveprompt {
  margin-top: 8px;
  background: rgba(0, 0, 0, 0.06);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.16);
  padding: 11px 13px;
  font-family: var(--font-mono);
  font-size: 0.78125rem; line-height: 1.55;
  color: var(--text);
  min-height: 64px; max-height: 122px; overflow-y: auto;
  white-space: pre-wrap; word-break: break-word;
  cursor: default; -webkit-user-select: text; user-select: text;
}
.t2v-liveprompt-empty { color: var(--text-faint); font-style: italic; }

/* ── Autofill safety net (secondary to the semantic name/id/autocomplete fixes;
   see docs/input-autofill-audit.md) ─────────────────────────────────────────
   If a browser still force-autofills a creative/utility field, keep it looking
   like the app's own input — the value stays readable, no jarring credential
   fill colour. Scoped to the creative input class only; auth fields use their
   own id-styled inputs, so their legitimate autofill styling is untouched. This
   is NOT a substitute for correct field semantics. */
.text-input:-webkit-autofill,
.text-input:-webkit-autofill:hover,
.text-input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: 0 0 0 1000px var(--surface-2) inset;
  caret-color: var(--text);
  transition: background-color 100000s ease-out 0s;
}

/* ============================================================================
   iOS SAFARI — background-attachment:fixed white-strip fix (root cause)
   ----------------------------------------------------------------------------
   body and main paint their gradient/dot-grid with background-attachment:fixed
   (positioned to the viewport, not the element). iOS Safari does NOT repaint a
   fixed background when its toolbar shows/hides and the visual viewport resizes,
   so the newly-revealed strip at the bottom is left UNPAINTED — the browser's
   default white — and it sat over the Workflow bottom tool dock, hiding it.
   On iOS only (@supports for a WebKit-touch property), paint these backgrounds
   with their own box (scroll) instead: there is then nothing for a viewport
   resize to leave behind, so no white strip. Desktop keeps the intended fixed
   full-bleed treatment untouched. !important + matched specificity so it beats
   the earlier !important fixed declarations. */
@supports (-webkit-touch-callout: none) {
  body,
  main,
  main:has(#view-avatar.active),
  main:has(#view-contact.active),
  main:has(#view-admin.active) {
    background-attachment: scroll !important;
  }
}

/* ---------------------------------------------------- Backroom pitch ------
   The "sell it" block on the pricing page. On-brand: silver surface, Archivo
   headline, black primary CTA, silver download button. Product name only —
   avoids the platform name so a rebrand doesn't touch it. */
.backroom-pitch {
  border: 1px solid var(--border-soft);
  border-radius: 14px;
  background: var(--surface-2);
  padding: 26px 26px 22px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.backroom-pitch-eyebrow {
  font-family: var(--font-ui);
  font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--text-faint); margin-bottom: 10px;
}
.backroom-pitch-title {
  font-family: var(--font-display);
  font-size: clamp(24px, 4vw, 34px); font-weight: 700; line-height: 1.05; letter-spacing: -0.02em;
  color: var(--text); margin: 0 0 10px;
}
.backroom-pitch-lede {
  font-family: var(--font-ui);
  font-size: 0.9375rem; line-height: 1.55; color: var(--text-muted);
  max-width: 62ch; margin: 0 0 20px;
}
.backroom-pitch-features {
  list-style: none; margin: 0 0 22px; padding: 0;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 26px;
}
.backroom-pitch-features li { display: flex; flex-direction: column; gap: 3px; }
.bpf-k { font-size: 0.84375rem; font-weight: 650; color: var(--text); }
.bpf-v { font-size: 0.8125rem; line-height: 1.45; color: var(--text-muted); }
.backroom-pitch-cta { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 12px; }
.backroom-btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 11px 20px; border-radius: 999px; font-family: var(--font-ui);
  font-size: 0.875rem; font-weight: 600; cursor: pointer; text-decoration: none;
  border: 1px solid transparent;
  transition: transform 120ms ease, background 120ms ease, box-shadow 120ms ease;
}
.backroom-btn:active { transform: translateY(1px); }
.backroom-btn-primary { background: var(--accent); color: #fff; }
.backroom-btn-primary:hover { box-shadow: 0 4px 16px rgba(10, 10, 10, 0.25); }
.backroom-btn-download { background: var(--surface); color: var(--text); border-color: var(--border-soft); }
.backroom-btn-download:hover { background: var(--silver-polished); }
.backroom-badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px; border-radius: 999px;
  background: var(--accent-soft); color: var(--text);
  font-size: 0.8125rem; font-weight: 650;
}
.backroom-soon { font-size: 0.8125rem; color: var(--text-faint); }
.backroom-pitch-fineprint { font-size: 0.75rem; color: var(--text-faint); margin: 0; }
@media (max-width: 620px) {
  .backroom-pitch-features { grid-template-columns: 1fr; }
  .backroom-pitch { padding: 20px; }
}

/* Admin: Backroom access toggle — filled when access is ON (button reads
   "Revoke"), outline when OFF ("Grant"). */
.admin-backroom-btn.is-on { background: var(--accent); color: #fff; border-color: var(--accent); }

/* ═══════════════════════════════════════════════════════════════════════════
   READABILITY ON THE ONYX GROUND (impeccable harden, 2026-09-10)
   The shipped default canvas tone is 0% (near-black ground). Library-page
   headings, empty states, and section labels were still painted with the
   chassis ink (#111) meant for the silver surfaces and measured 1.0–1.2:1,
   i.e. invisible. Anything that sits DIRECTLY on the ground takes the
   viewfinder text tokens here; text inside a silver panel/card is untouched.
   ═══════════════════════════════════════════════════════════════════════════ */
/* Onyx is the shipped default, so these apply unconditionally (no dependence
   on the data-app-lum attribute JS sets after load); the light tone stops
   restore chassis ink below. */
.projects-empty-title,
.collections-empty-block .projects-empty-title,
.explore-row-empty .projects-empty-title,
.explore-row-head h3 { color: var(--vf-text); }
.explore-row-icon { color: var(--vf-text-label); }
.projects-empty .section-hint,
.collections-empty-block .section-hint,
.explore-row-empty .section-hint,
.projects-empty-glyph { color: var(--vf-text-muted); }
.collections-section > .section-label-row .section-label,
#explore-rows > .section-label { color: var(--vf-text-label); }
#app-shell[data-app-lum="light"] :is(.projects-empty-title, .explore-row-head h3) { color: var(--text); }
#app-shell[data-app-lum="light"] :is(.explore-row-icon, .collections-section > .section-label-row .section-label, #explore-rows > .section-label) { color: var(--text-label); }
#app-shell[data-app-lum="light"] :is(.projects-empty .section-hint, .collections-empty-block .section-hint, .explore-row-empty .section-hint, .projects-empty-glyph) { color: var(--text-muted); }

/* Moodboard cards sit on the silver card surface but inherited the shell's
   light ink (#EDEDF0 on #D8DDE1 = 1.17:1). Name takes chassis ink; the READY
   pill's green was 2.97:1 on its soft fill, so it darkens to a legible green. */
.moodboard-card-name { color: var(--text); }
.moodboard-status-pill.ready { color: #1f6b3c; background: rgba(47, 143, 82, 0.18); }

/* Viewfinder empty copy read at ~3.3:1 (--vf-text-faint on --vf-bg). */
.viewfinder-empty p { color: var(--vf-text-muted); }

/* ── Keyboard focus you can actually see ──────────────────────────────────
   The old ring was `2px solid var(--accent)` = black on a black button on a
   black ground. One two-tone ring for every interactive atom: an inner halo
   in the surface colour, an outer ring in ink, so it reads on silver AND on
   the Onyx ground without knowing which one it is on. */
:is(button, [role="button"], a[href], input, select, textarea, summary, [tabindex]:not([tabindex="-1"])):focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--surface-2), 0 0 0 4px var(--pure-black);
}
#app-shell[data-app-lum="dark"] :is(button, [role="button"], a[href], summary, [tabindex]:not([tabindex="-1"])):not(.panel *):not(.entity-card *):focus-visible {
  box-shadow: 0 0 0 2px var(--app-ground, #0a0a0c), 0 0 0 4px #FFFFFF;
}
.primary-btn:focus-visible { box-shadow: 0 0 0 2px var(--surface-2), 0 0 0 4px var(--pure-black), 0 6px 20px -6px rgba(0, 0, 0, 0.55); }

/* ── The status line is the one sentence about your credits ────────────────
   It was the smallest text on the page (mono, 12.5px, muted). It now reads as
   a sentence: UI face, 13px, full ink, and a leading mark that carries state
   without relying on colour alone. */
.status-line { font-family: var(--font-ui); font-size: 0.8125rem; color: var(--text); line-height: 1.45; text-transform: none; letter-spacing: 0; }
.status-line:empty { min-height: 0; margin-top: 0; }
.status-line.error { color: var(--error); font-weight: 600; }
.status-line.error::before { content: "⚠ "; }
.status-line.success { color: var(--success); font-weight: 600; }
.status-line.success::before { content: "✓ "; }

/* ── The sentence under every Generate ─────────────────────────────────────
   Balance now → balance after, and the refund promise, at the moment of
   spend rather than after a cancel. Rendered by renderSpendLine (app.js). */
.spend-line { margin: 10px 0 0; font-family: var(--font-ui); font-size: 0.78125rem; line-height: 1.45; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.spend-line b { font-weight: 600; color: var(--text); }
.spend-line.short { color: var(--error); }
.spend-line.short b { color: var(--error); }
.spend-line-link { color: var(--text); font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }

/* ═══════════════════════════════════════════════════════════════════════════
   TYPE FLOOR + READING COPY (impeccable typeset/adapt, chunk 3)
   The detector logged 720 pieces of UI text under 11px. Chips, tags, badges,
   readouts and labels take an 11px floor here (the workflow canvas is left
   alone on purpose: it is its own instrument and out of scope). Helper copy
   moves OUT of the mono face: mono is for labels and readouts, not for
   sentences people have to read at arm's length.
   ═══════════════════════════════════════════════════════════════════════════ */
:is(#view-remaster label, #view-video label, #view-avatar label, .section-label, .field > label,
    .badge, .image-tag-chip, .image-tag-add-btn, .entity-card-tag, .explore-published-tag,
    .moodboard-status-pill, .moodboard-active-badge, .summary-chip, .char-summary-label,
    .scene-subgroup-title, .media-type-label, .style-locked-label, .gallery-item .tag,
    .explore-card-meta, .admin-analytics-table th, .table-subtext, .account-sub,
    .account-plan-card-rollover, .admin-contact-unread-badge, .admin-stuck-job-status,
    .appbar-account .rail-account-credits, .job-error-code, .job-error-request-id,
    .live-prompt-actions .text-btn, .nlr-model, .recent-item .recent-sub, .scene-upload-tile,
    .sd-inline-card label, .sd-lib-sub, .verify-banner-icon, .admin-mini-stat-label,
    .elements-page-card-project, .rail-footer-label,
    .rail-group-label, .nlr-inferred-mode, .pill-toggle-sub, .t2v-live-badge, .vf-status,
    .viewfinder-head, .btn-credit, .analytics-bar-label, .appbar-tone-pct,
    .app-tone .app-tone-name, .attr-group > label):not(#view-workflow *) { font-size: 0.6875rem; }
:is(#view-image, #view-video, #view-avatar, #view-characters, #view-charsheet, #view-scene,
    #view-storyboard, #view-shot-extractor, #view-remaster) .generate-helper { font-size: 0.75rem; line-height: 1.45; }
.section-hint { font-family: var(--font-ui); font-size: 0.78125rem; line-height: 1.5; letter-spacing: 0; text-transform: none; }

/* ── Mobile: the four Video modes wrapped to three lines each in 57px cells ──
   Two columns, whole words. The last control on every page also clears the
   floating agent button, which was covering the Model select. */
@media (max-width: 640px) {
  #video-mode-toggle { display: grid; grid-template-columns: 1fr 1fr; gap: 3px; }
  #video-mode-toggle .seg-btn { white-space: nowrap; padding: 8px 6px; }
}
@media (max-width: 860px) {
  #app-shell section.view, .app section.view, body section.view { padding-bottom: 96px; }
  /* These two carry their own !important padding block further up. */
  #view-shot-extractor.active, #view-remaster.active { padding-bottom: 96px !important; }
}
/* Vault placard chips and "Avoid" eyebrows (9px), and the viewfinder's
   "idle/running" tag (9px): same 11px floor as everything else in the chrome. */
#view-vault .chip, #view-vault .avoid-label, .viewfinder-tag { font-size: 0.6875rem; }
/* The static "library will grow here" block is `hidden` in the markup and the
   grid renders its own empty state; a display rule above was overriding the
   attribute, so the message showed twice once its title became visible. */
#elements-grow-hint[hidden] { display: none !important; }


/* ── A failure is stated on the plate, not only under the button ──────────── */
.viewfinder-empty.is-error p { color: #e07a7a; }
.viewfinder-empty.is-error .viewfinder-empty-glyph { color: #e07a7a; opacity: 0.9; }

/* ── Workflow canvas: the same 11px floor as the rest of the chrome ───────── */
#view-workflow :is(.node-field-label, .wf-preset-chip, .node-head, .tb-icon-btn, .node-batch-stepper,
  .node-enhance-undo, .wf-file-label, .wf-cov-chip, .wf-cov-count, .wf-cov-eyebrow, .wf-cov-lbl,
  .wf-lock-add, .wf-rec-acts button, .wf-cast-acts button, .wf-rec-save button, .wf-cg-cat,
  .wf-cg-panel-eyebrow, .wf-rec-desc, .node-ref-chip, .node-head-meta, .nlr-model, .nlr-inferred-mode,
  .node-zoom-caret, .node-count-pill, .wf-lock-x) { font-size: 0.6875rem; }
#view-workflow .node.selected .node-overlay .nlr-cost { font-size: 0.6875rem; }


/* ── Studio hub, grouped by stage of the pipeline ─────────────────────────── */
.hub-stage { margin: 0 0 30px; }
.hub-stage-head { display: flex; align-items: baseline; gap: 12px; margin: 0 0 12px; }
.hub-stage-name { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: 0.9375rem; letter-spacing: 0.01em; color: var(--vf-text); }
.hub-stage-line { font-family: var(--font-ui); font-size: 0.78125rem; color: var(--vf-text-muted); }
#app-shell[data-app-lum="light"] .hub-stage-name { color: var(--text); }
#app-shell[data-app-lum="light"] .hub-stage-line { color: var(--text-muted); }
.hub-stage .studio-hub-grid { margin: 0; }
.hub-stage .project-hub-tile-desc { margin-top: 6px; }
/* Workflow leftovers below the floor (some render outside #view-workflow). */
.node-zoom-caret, .wf-tool-key, .wf-mb-label, .nlr-cost, .node-enhance-btn,
#view-workflow :is(.node-zoom-caret, .wf-tool-key, .wf-mb-label, .nlr-cost, .node-enhance-btn) { font-size: 0.6875rem; }
/* Three workflow readouts carry a more specific rule than the floor above. */
.node-enhance-btn, .wf-mb-label, .node-zoom-caret { font-size: 0.6875rem !important; }
/* Hub tile labels/descriptions sit on the ground, not on a card. */
.project-hub-tile-desc { color: var(--vf-text-muted); }
.project-hub-tile-label { color: var(--vf-text); }
#app-shell[data-app-lum="light"] .project-hub-tile-desc { color: var(--text-muted); }
#app-shell[data-app-lum="light"] .project-hub-tile-label { color: var(--text); }
/* Vault placard meta/ids and the Upscale viewfinder title: 11px floor. */
#view-vault :is(.meta-left, .meta-right, .placard-id, .placard-meta), .viewfinder-title { font-size: 0.6875rem; }


/* ── Hub v3: continuous grid, tile square untouched, description under it ── */
.hub-cell { display: flex; flex-direction: column; align-items: stretch; min-width: 0; }
.hub-cell .project-hub-tile { width: 100%; }
.hub-cell-desc { margin: 10px 8px 0; text-align: center; font-family: var(--font-ui); font-size: 0.78125rem; line-height: 1.4; color: var(--vf-text-muted); }
#app-shell[data-app-lum="light"] .hub-cell-desc { color: var(--text-muted); }
.hub-cell.hidden { display: none; }
/* Each grid track is wider than the tile and items are centred in it, so a
   cell that grew to its longest description line drifted off the column.
   Cells are exactly the tile's width; the description wraps inside it. */
#view-studio-hub .hub-cell { width: 100%; max-width: 330px; justify-self: center; }
/* Phones: one column; the cell fills it so the tile is as wide as before
   and sits centred (a fixed 330px cell in a 1fr track was pinned left). */
@media (max-width: 640px) {
  #view-studio-hub .hub-cell { max-width: none; }
}

/* ── Quiet pill for every .text-btn (Replace / Remove / Undo / Copy / Save) ──
   These rendered as bare native buttons. Same family as the segmented pills:
   hairline, 999px, mono-free UI face; Remove goes danger-red on hover. */
.text-btn {
  appearance: none; background: rgba(255, 255, 255, 0.35); border: 1px solid var(--border-soft);
  color: var(--text-muted); font-family: var(--font-ui); font-size: 0.78125rem; font-weight: 600;
  padding: 7px 14px; border-radius: 999px; cursor: pointer; line-height: 1.2;
  box-shadow: var(--inset-hairline);
  transition: color .15s ease, border-color .15s ease, background-color .15s ease, transform .1s ease;
}
.text-btn:hover { color: var(--text); border-color: var(--accent); background: rgba(255, 255, 255, 0.6); transform: translateY(-1px); }
.text-btn:active { transform: translateY(1px); }
.text-btn:disabled { opacity: .5; cursor: not-allowed; transform: none; }
.text-btn[id$="-remove-btn"]:hover { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); }
/* The action row only exists once an image is attached; a display rule
   above was overriding the `hidden` attribute, so empty-state pages showed
   Replace / Remove with nothing to replace. */
.avatar-slot-actions[hidden] { display: none; }


/* ═══ Canvas-theme slider: tick meter ═════════════════════════════════════
   A dark pill bed with fine vertical ticks. Ticks left of the thumb are lit,
   ticks to the right stay dim, so the position reads without a number. The
   thumb is a slim bright bar with a soft halo. --pct is set by
   applyCanvasThemePct. Track pseudo-elements are styled explicitly so no
   browser default (a flat white track) can show through. */
#appbar-tone-slider {
  -webkit-appearance: none; appearance: none; width: 100%; height: 30px; margin: 0; padding: 0; cursor: pointer;
  --tick-dim: rgba(255, 255, 255, 0.16);
  --tick-lit: rgba(255, 255, 255, 0.78);
  --pct: 0%;
  background:
    linear-gradient(90deg, transparent var(--pct), #0c0c0f var(--pct)),
    repeating-linear-gradient(90deg, var(--tick-lit) 0 1.5px, transparent 1.5px 8px),
    #0c0c0f;
  background-blend-mode: normal;
  background-position: 0 0, 6px center, 0 0;
  background-size: 100% 100%, calc(100% - 12px) 12px, 100% 100%;
  background-repeat: no-repeat, repeat-x, no-repeat;
  border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.10);
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.7), inset 0 -1px 0 rgba(255, 255, 255, 0.04);
}
/* dim ticks under everything, painted by the track pseudo so the lit layer above can be clipped at --pct */
#appbar-tone-slider::-webkit-slider-runnable-track {
  height: 30px; border-radius: 999px; background: transparent; border: 0;
  background-image: repeating-linear-gradient(90deg, var(--tick-dim) 0 1.5px, transparent 1.5px 8px);
  background-size: calc(100% - 12px) 12px; background-position: 6px center; background-repeat: repeat-x;
}
#appbar-tone-slider::-moz-range-track {
  height: 30px; border-radius: 999px; background: transparent; border: 0;
  background-image: repeating-linear-gradient(90deg, var(--tick-dim) 0 1.5px, transparent 1.5px 8px);
  background-size: calc(100% - 12px) 12px; background-position: 6px center; background-repeat: repeat-x;
}
#appbar-tone-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 5px; height: 20px; margin-top: 4px; border-radius: 3px;
  background: #f7f8f8; border: 0;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 10px rgba(255, 255, 255, 0.45), 0 0 22px rgba(255, 255, 255, 0.18);
  cursor: grab; transition: box-shadow .15s ease;
}
#appbar-tone-slider::-moz-range-thumb {
  width: 5px; height: 20px; border-radius: 3px; background: #f7f8f8; border: 0;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 0 10px rgba(255, 255, 255, 0.45), 0 0 22px rgba(255, 255, 255, 0.18);
  cursor: grab;
}
#appbar-tone-slider:hover::-webkit-slider-thumb { box-shadow: 0 0 0 1px rgba(0,0,0,.6), 0 0 14px rgba(255,255,255,.6), 0 0 28px rgba(255,255,255,.25); }
#appbar-tone-slider::-webkit-slider-thumb:active { cursor: grabbing; }
#appbar-tone-slider:focus-visible { outline: none; box-shadow: inset 0 2px 6px rgba(0,0,0,.7), 0 0 0 2px #0c0c0f, 0 0 0 4px rgba(255,255,255,.5); }

/* ═══════════════════════════════════════════════════════════════════════════
   WORKFLOW NODES: glass surface, hairlines, mono eyebrow, halo (Plumar study,
   in ShotBorn's own silver). Colour is still identification only.
   ═══════════════════════════════════════════════════════════════════════════ */
#view-workflow {
  --wf-ease: cubic-bezier(0.32, 0.72, 0, 1);
  --wf-ease-sheet: cubic-bezier(0.34, 1.28, 0.48, 1);
  --wf-dur-fast: .16s; --wf-dur: .24s; --wf-dur-slow: .36s;
}
#view-workflow .node {
  background:
    radial-gradient(130% 85% at 78% -10%, rgba(255,255,255,0.10) 0%, rgba(255,255,255,0.03) 26%, transparent 56%),
    linear-gradient(157deg, rgba(255,255,255,0.045), rgba(255,255,255,0.015) 58%, rgba(255,255,255,0.01)),
    var(--wf-surface);
  border: 1px solid rgba(255,255,255,0.09);
  box-shadow:
    0 30px 70px rgba(0,0,0,0.55), 0 2px 10px rgba(0,0,0,0.4),
    inset 0 1px 0 rgba(255,255,255,0.16),        /* bright top hairline */
    inset 0 0 0 1px rgba(255,255,255,0.03),
    inset 0 -1px 0 rgba(0,0,0,0.45);              /* dark bottom hairline */
  transition: box-shadow var(--wf-dur) var(--wf-ease), border-color var(--wf-dur) var(--wf-ease), transform var(--wf-dur-fast) var(--wf-ease);
}
#view-workflow .node.selected {
  border-color: color-mix(in srgb, var(--node-color, #ffffff) 55%, transparent);
  box-shadow:
    0 30px 70px rgba(0,0,0,0.6), 0 2px 10px rgba(0,0,0,0.4),
    inset 0 1px 0 color-mix(in srgb, var(--node-color, #ffffff) 45%, transparent),
    inset 0 0 0 1px rgba(255,255,255,0.05),
    inset 0 -1px 0 rgba(0,0,0,0.45),
    0 0 0 1px color-mix(in srgb, var(--node-color, #ffffff) 30%, transparent),
    0 0 24px -6px color-mix(in srgb, var(--node-color, #ffffff) 45%, transparent);
}
#view-workflow .node.dragging { transform: scale(1.01); }

/* Mono eyebrow in the node's hue above the title. Preset/doc nodes get it
   as a full-width first line of the head; generator nodes already carry a
   type word in the label row, which becomes the eyebrow. */
#view-workflow .node-head { flex-wrap: wrap; row-gap: 2px; padding: 12px 14px 8px; background: transparent; border: 0; border-bottom: 1px solid var(--wf-line); }
#view-workflow .node-head::before {
  content: ""; flex-basis: 100%; order: -1;
  font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--node-color, var(--wf-ink-dim)); line-height: 1;
}
#view-workflow .node-type-camera .node-head::before { content: "Camera"; }
#view-workflow .node-type-cinematographer .node-head::before { content: "Cinematographer"; }
#view-workflow .node-type-colorgrade .node-head::before { content: "Color grade"; }
#view-workflow .node-type-style .node-head::before { content: "Style"; }
#view-workflow .node-type-script .node-head::before { content: "Script"; }
#view-workflow .node-type-bible .node-head::before { content: "Show bible"; }
#view-workflow .node-type-coverage .node-head::before { content: "Coverage"; }
#view-workflow .node-type-note .node-head::before { content: "Note"; }
#view-workflow .node-type-prompt .node-head::before { content: "Prompt"; }
#view-workflow .node-type-reference .node-head::before { content: "Reference"; }
#view-workflow .node-head-label, #view-workflow .tb-name { font-family: var(--font-ui); font-size: 0.84375rem; font-weight: 650; letter-spacing: -0.015em; color: var(--wf-ink); }
#view-workflow .nlr-type { font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--node-color, var(--wf-ink-dim)); }
#view-workflow .nlr-model { font-size: 0.6875rem; color: var(--wf-ink-muted); }
#view-workflow .node-status-line { font-family: var(--font-mono); font-size: 0.6875rem; letter-spacing: 0.04em; color: var(--wf-ink-muted); }

/* Ports: inputs are hollow rings, the output is a solid dot in the hue —
   the wire that leaves it is the same colour, so a chain is traceable. */
#view-workflow .node-port-out { background: var(--node-color, var(--wf-ink)); border-color: var(--node-color, var(--wf-ink)); }
#view-workflow .node-port-in.connected { background: var(--node-color, var(--wf-ink)); }

/* Footer strip: status left, actions right, one hairline above (same shape
   as the generator node's bottom row, now shared by every node kind). */
#view-workflow .node .node-botrow { border-top: 1px solid var(--wf-line); }

/* Sheets and menus rise with the spring; nothing pops. */
@keyframes wfSheetIn { from { opacity: 0; transform: translateY(6px) scale(0.975); } to { opacity: 1; transform: none; } }
#view-workflow .node.selected .node-toolbar,
#view-workflow .node.selected .node-label-row { animation: wfSheetIn var(--wf-dur-slow) var(--wf-ease-sheet) both; }
.node-zoom-menu:not(.hidden), .wf-moodboard-menu:not(.hidden) { animation: wfSheetIn var(--wf-dur) var(--wf-ease-sheet) both; }
#view-workflow .wf-tool-btn { transition: background-color var(--wf-dur-fast) var(--wf-ease), color var(--wf-dur-fast) var(--wf-ease), transform var(--wf-dur-fast) var(--wf-ease); }
#view-workflow .wf-tool-btn:active { transform: translateY(1px) scale(0.98); }
@media (prefers-reduced-motion: reduce) { #view-workflow * { animation: none !important; transition: none !important; } }

/* ── Page titles: tighter display tracking (typeset) ────────────────────── */
h1 { letter-spacing: -0.03em; }
.panel-head h1, .projects-head-row h1 { letter-spacing: -0.03em; }

/* ═══ Section rail (long pages): tiny uppercase labels with a dot for the
   current section, down the left gutter. Built by initSectionRails (app.js)
   from each view's own headings; desktop only. ═══════════════════════════ */
.section-rail { position: fixed; right: 28px; top: 50%; transform: translateY(-50%); z-index: 30; display: flex; flex-direction: column; align-items: flex-end; gap: 14px; pointer-events: auto; }
.section-rail a { display: flex; flex-direction: row-reverse; align-items: center; gap: 10px; text-decoration: none; font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--vf-text-faint); transition: color .18s ease; }
.section-rail a::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: .55; transition: transform .24s cubic-bezier(.32,.72,0,1), opacity .18s ease; }
.section-rail a:hover { color: var(--vf-text-muted); }
.section-rail a.is-active { color: var(--vf-text); }
.section-rail a.is-active::before { opacity: 1; transform: scale(1.5); }
#app-shell[data-app-lum="light"] .section-rail a { color: var(--text-faint); }
#app-shell[data-app-lum="light"] .section-rail a.is-active { color: var(--text); }
@media (max-width: 1240px) { .section-rail { display: none; } }

/* Signup consent + account data controls */
.auth-consent { display: flex; gap: 10px; align-items: flex-start; font-size: 0.78125rem; line-height: 1.45; color: var(--muted); margin: 6px 0 12px; cursor: pointer; }
.auth-consent input { margin-top: 3px; flex: none; }
.auth-consent a { color: inherit; text-decoration: underline; }
.settings-btn-row { display: flex; gap: 10px; flex-wrap: wrap; }
.settings-help { font-size: 0.78125rem; color: var(--muted); line-height: 1.5; margin: 0 0 10px; max-width: 60ch; }
.danger-btn { border-color: #7a2e2e; color: #e3a1a1; }
.danger-btn:hover { background: rgba(160, 50, 50, 0.15); }

/* Legal / consent copy (security pass C) */
.upload-consent-note {
  margin: 0 0 12px; padding: 8px 10px; border-radius: 8px;
  background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border, #28282f);
  color: var(--text-muted, #93939c); font-size: 0.75rem; line-height: 1.5;
}
.upload-consent-note a { color: inherit; text-decoration: underline; }
.legal-links { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-top: 8px; }
.legal-links a { color: var(--text-muted, #93939c); font-size: 0.8125rem; text-decoration: none; border-bottom: 1px solid transparent; }
.legal-links a:hover { color: var(--text, #f2f2f0); border-bottom-color: currentColor; }
.account-topup-card-note { color: var(--text-muted, #93939c); font-size: 0.71875rem; margin: 4px 0 8px; }
.pw-wrap { position: relative; }
.pw-wrap input { padding-right: 58px; }
.pw-toggle {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  background: transparent; border: 1px solid var(--border, #28282f); border-radius: 6px;
  color: var(--text-muted, #93939c); font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.3px; padding: 3px 8px; cursor: pointer;
}
.pw-toggle:hover { color: var(--text, #f2f2f0); border-color: var(--text-muted, #93939c); }

/* Video page camera presets (Sept 2026): Scene-style card rows inside the
   text-to-video worksheet. The rows scroll sideways inside the flyout; the
   speed pills sit under the move row; the booster reuses the Scene toggle. */
.t2v-presets .visual-selector { margin-top: 2px; }
.t2v-preset-speed { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
.t2v-preset-speed-label { font-family: var(--font-mono, inherit); font-size: 0.6875rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); }
.t2v-presets .pill-row { margin-top: 4px; }
.t2v-booster-row { margin-top: 2px; }
/* Text-to-Video worksheet hierarchy: two groups (SHOT DESIGN, SHOT
   DESCRIPTION) above the category labels. The group title is the category
   label's own family (mono, uppercase, the label tracking token) one step
   larger and heavier; its helper is the ordinary .section-hint. Spacing
   reuses the rhythm already in this panel: 4px label-to-hint, 16px between
   categories, 28px around the one divider (the scene-subgroup rhythm). */
.t2v-group-head { margin: 0 0 18px; }
.t2v-group-title {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: var(--text-label-track, 0.8px);
  text-transform: uppercase;
  /* The panel ink, like the labels under it: the root label token is the
     dark-page value and reads near-white on this light panel. */
  color: inherit;
  margin: 0 0 4px;
}
.t2v-group-head .section-hint { margin: 0; }
.t2v-group-head + .t2v-fieldrow { margin-top: 0; }
.t2v-group-divider {
  /* The major break, drawn at 60% ink: heavier than the soft internal
     separators, without becoming the loudest mark on a restrained panel. */
  border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  margin: 28px 0;
}

/* --- Panel a11y + craft pass (Sept 2026) ---------------------------------
   Type here is expressed in rem so it follows the reader's own text-size
   setting instead of ignoring it. At the default 16px root these render at
   exactly the pixel sizes they replace (11 / 12.5 / 13 / 12px), so nothing
   moves for anyone who has not changed the setting. */
.t2v-field { --t2v-label: 0.6875rem; --t2v-hint: 0.78rem; --t2v-body: 0.8125rem; }
.t2v-field .section-label { font-size: var(--t2v-label) !important; }
.t2v-field .section-hint,
.t2v-field .scene-colorgrade-desc,
.t2v-preset-speed-label { font-size: var(--t2v-hint) !important; }
.t2v-field .text-input,
.t2v-field .visual-card-label,
.t2v-field .pill-toggle { font-size: var(--t2v-body) !important; }
.t2v-group-title { font-size: 0.75rem !important; }
.t2v-field .visual-card { height: auto; min-height: 8.5rem; }

/* The edge of a scrollable row fades, so a half-visible card says "there is
   more this way" before the pointer ever arrives. */
.t2v-presets .visual-selector.scrollable:not(.at-end) .visual-selector-track {
  mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent 100%);
}
.t2v-presets .visual-selector.scrollable:not(.at-start):not(.at-end) .visual-selector-track {
  mask-image: linear-gradient(to right, transparent 0, #000 34px, #000 calc(100% - 34px), transparent 100%);
}
.t2v-presets .visual-selector.scrollable:not(.at-start).at-end .visual-selector-track {
  mask-image: linear-gradient(to right, transparent 0, #000 34px);
}

/* "None" is a real choice, not a missing picture: its placard carries a
   dash so it never reads as a thumbnail that failed to load. */
/* The placeholder's own ::before draws a picture glyph, which on a None
   card reads as a thumbnail that failed to load. Swap that glyph for a dash
   on None only. ::after is left alone — it carries the selected checkmark. */
.visual-card-none .visual-card-thumb-placeholder::before {
  content: '—';
  width: auto; height: auto;
  background: none;
  -webkit-mask: none; mask: none;
  opacity: 0.55;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: 1.05rem; line-height: 1;
}

/* Speed modifies the move above it; it is not a category of its own. */
.t2v-preset-speed { padding-left: 12px; border-left: 1px solid var(--border-soft); }

/* The switch keeps its drawn size; only its hit region grows to the
   desktop default. */
.t2v-booster-row .toggle-switch { min-height: 28px; display: inline-flex; align-items: center; }


/* ── HIG pass, Sept 2026: measured contrast lifts ──────────────────────────
   Each value below is the lightest (or darkest) ink that clears 4.5:1 on the
   surface it actually sits on, computed from the live composited colours. */

/* Form section labels on the silver panel were 4.03:1 at 11px. */
:is(#view-settings, #view-profile, #view-contact, #view-admin, #view-avatar, .panel)
  :is(.section-label, .field > label) { color: #6a6c6e; }

/* The projects/elements search glyph was 1.78:1 on the dark ground — it read
   as decoration rather than an affordance. */
.projects-search-icon { color: #8a8d93; }

/* The card overflow menu was 1.45:1 against the card it sits on. */
.elements-page-card-menu, .moodboard-card-menu-btn { color: #e8eaec; }

/* Workflow's canvas eyebrows and counts were 3.80–4.08:1 on the node ground. */
.wf-cov-eyebrow, .wf-cg-eyebrow, .wf-cov-count { color: #8f9196; }

/* Workflow's two smallest strings were below the 10pt desktop minimum. */
.wf-file-label { font-size: 0.6875rem; }
.wf-cov-build { font-size: 0.6875rem; }

/* History was an <h3> under an <h1> — a skipped level. It is now an <h2> and
   keeps the h3's exact appearance, so the outline is correct and nothing on
   screen moves. */
.history-block .history-heading { font: inherit; margin: 0; }
.history-block h3, .history-block .history-heading {
  font-family: var(--font-display); font-size: 0.9375rem; font-weight: 700;
  letter-spacing: 0.01em; margin: 0 0 14px; color: inherit;
}

/* ── HIG pass 2: the five that survived, each measured on its own ground ───
   Scene's viewfinder and toolbar sit on the SILVER panel, not the dark one,
   so the shared faint token reads the wrong way round there. These are the
   scoped corrections rather than a change to the shared token. */

/* Scene's placeholder + download glyph are on #eef1f4, not the dark ground. */
#view-scene .viewfinder-empty,
#view-scene .viewfinder-empty p { color: #6c6c6c; }
#view-scene .icon-btn { color: #5f6164; }

/* The Live Prompt label sits on the raised silver #d8dde1. */
.live-prompt-head .section-label { color: #5e6062; }

/* READY was 3.98:1 green-on-mint; this green clears 4.6:1 on the same pill. */
.moodboard-status-pill.ready { color: #14512c; }

/* The workflow canvas zoom control was 1.98:1 on the dark ground. */
.node-zoom-side-btn { color: #8c8f94; }

/* The shared page notice that replaced window.alert. Same ink and mono face
   as the status lines it stands in for; it sits above the workspace, never
   over the controls, and does not take focus. */
.app-notice {
  position: fixed;
  left: 50%;
  bottom: 28px;
  transform: translate(-50%, 12px);
  z-index: 900;
  max-width: min(560px, calc(100vw - 48px));
  padding: 12px 18px;
  border-radius: 8px;
  background: #16181c;
  border: 1px solid rgba(255, 255, 255, 0.16);
  box-shadow: 0 10px 34px -12px rgba(0, 0, 0, 0.8);
  color: #edeef0;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  line-height: 1.5;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.14s ease, transform 0.14s ease;
}
.app-notice.show { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
.app-notice.error { border-color: rgba(229, 72, 77, 0.55); }
@media (prefers-reduced-motion: reduce) { .app-notice { transition: none; } }

/* ── Phone fixes, Sept 2026 ────────────────────────────────────────────────
   Two things reported from an iPhone: the admin charts ran off the side of
   the screen, and the Elements thumbnails had shrunk to nothing. */

/* CHARTS. .analytics-chart is a grid item, and a grid item's default
   min-width:auto means it cannot be narrower than its contents. The bar
   columns never shrink (flex-shrink 0), so twenty of them pushed the card
   wider than the phone instead of scrolling inside it. Letting the item and
   the bar track be narrower than their contents makes the existing
   overflow-x:auto do its job. */
.analytics-chart { min-width: 0; }
.analytics-chart-bars { min-width: 0; scrollbar-width: thin; }

/* The rotated date labels share the bar track's fixed height, so a long label
   ate into the bars. The track keeps its height for the percentage maths and
   the labels get reserved space beneath it. */
.analytics-chart-bars { height: auto; padding-bottom: 44px; }
.analytics-bar-col { height: 120px; position: relative; }
.analytics-bar-label { position: absolute; top: 100%; left: 50%; transform: translateX(-50%) rotate(180deg); margin-top: 6px; }

@media (max-width: 640px) {
  /* Four columns suits a bare thumbnail wall. These cards carry a name, a
     date and a project under the picture, and at 77px wide that label grew
     taller than the whole card and squeezed the thumbnail to zero height. */
  .card-grid.elements-lib-grid,
  .card-grid.projects-grid,
  .card-grid.moodboards-grid { grid-template-columns: repeat(2, 1fr); }
}

/* The picture is the point of an element card, so it takes a fixed share of
   the card rather than whatever the label leaves over. The card grows to fit
   its own label instead of being pinned to a portrait ratio. */
.elements-page-card { aspect-ratio: auto; height: auto; min-height: 0; }
.elements-page-card-thumb { flex: none; aspect-ratio: 1 / 1; min-height: 84px; }

/* Feature flags on the admin page. One card per flag; the account list only
   matters in "accounts" mode, so it is shown only then. */
.admin-flags-list { display: grid; gap: 14px; }
.admin-flag { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; display: grid; gap: 10px; }
.admin-flag-head { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; flex-wrap: wrap; }
.admin-flag-name { font-weight: 700; }
.admin-flag-source { font-size: 0.75rem; color: var(--text-muted, #8a9099); }
.admin-flag-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.admin-flag-row select { min-width: 240px; }
.admin-flag textarea { width: 100%; min-height: 84px; font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.8125rem; box-sizing: border-box; }
.admin-flag-status { font-size: 0.8125rem; min-height: 1.2em; }
.admin-flag-status.is-error { color: var(--danger, #e64646); }
/* Visible to screen readers only. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* The app-wide button and select styles fill their container; in a flag card
   that makes Save a bar across the whole admin page. Size them to content. */
.admin-flag-row .primary-btn, .admin-flag-row .secondary-btn { width: auto; flex: 0 0 auto; }
.admin-flag-row select { width: auto; max-width: 100%; flex: 0 1 360px; }
/* Product funnel on the admin page: one bar per step, width is the share of
   the largest step so the drop-offs read at a glance. */
/* An explicit column: without one the grid's single track sizes to its widest
   content and runs past a phone screen however the rows inside are laid out. */
.admin-funnel { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; margin: 8px 0 18px; min-width: 0; }
/* Narrow screens: label and count on one line, the bar full width beneath.
   The count may wrap; forcing it onto one line is what pushed every row past
   the edge of a phone screen. */
.admin-funnel-step {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, max-content); grid-template-areas: "label count" "bar bar";
  column-gap: 12px; row-gap: 4px; align-items: baseline; font-size: 0.8125rem; min-width: 0;
}
.admin-funnel-step > span:first-child { grid-area: label; min-width: 0; }
.admin-funnel-bar { grid-area: bar; min-width: 0; }
.admin-funnel-count { grid-area: count; font-variant-numeric: tabular-nums; text-align: right; min-width: 0; }
.admin-funnel-count .table-subtext { display: inline; }
/* Wide screens: label, bar and count side by side. */
@media (min-width: 760px) {
  .admin-funnel { gap: 6px; }
  .admin-funnel-step { grid-template-columns: minmax(150px, 220px) minmax(0, 1fr) minmax(96px, max-content); grid-template-areas: "label bar count"; align-items: center; }
  .admin-funnel-count { white-space: nowrap; }
}
.admin-funnel-bar { height: 12px; border-radius: 3px; background: var(--surface-3, rgba(0,0,0,0.08)); overflow: hidden; }
.admin-funnel-fill { height: 100%; background: var(--text, #111); min-width: 2px; }
.admin-funnel-step.is-count .admin-funnel-fill { opacity: 0.35; }
.admin-funnel > p { margin: 4px 0 0; overflow-wrap: anywhere; }
.admin-funnel-tables { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: 18px; min-width: 0; }
.admin-funnel-tables > div { min-width: 0; }
.admin-funnel-tables td { font-variant-numeric: tabular-nums; }
/* The bar and its fill are spans; without a block display the width is ignored
   and every bar reads as full. */
.admin-funnel-bar, .admin-funnel-fill { display: block; }
/* Privacy policy notice. A light card fixed near the bottom of the window:
   the top of the page belongs to the floating toolbar, and the pages behind it
   range from black (Studio) to silver panels, so the card carries its own
   light surface and dark text instead of a tint that vanishes on black. It
   announces a change rather than an error, so it borrows no warning colour. */
.policy-notice {
  position: fixed; left: 50%; bottom: max(16px, env(safe-area-inset-bottom)); transform: translateX(-50%);
  z-index: 900; width: min(720px, calc(100vw - 32px)); box-sizing: border-box;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 12px 12px 16px; border-radius: 12px;
  background: #f4f5f7; color: #0a0a0b; border: 1px solid rgba(10, 10, 11, 0.12);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  font-size: 0.8125rem; line-height: 1.45;
}
.policy-notice-text { flex: 1 1 320px; }
.policy-notice-link { color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; white-space: nowrap; }
.policy-notice-close {
  flex: 0 0 auto; width: 32px; height: 32px; border-radius: 999px; border: 0; cursor: pointer;
  background: transparent; color: inherit; font-size: 1.25rem; line-height: 1;
}
.policy-notice-close:hover, .policy-notice-close:focus-visible { background: rgba(10, 10, 11, 0.08); outline: none; box-shadow: 0 0 0 2px rgba(10, 10, 11, 0.35); }
@media (pointer: coarse) { .policy-notice-close { width: 44px; height: 44px; } }

/* ═══════════ PRICING PLANS — large dark cards (Sept 2026) ═══════════
   Plan cards in the shape of a standard three-up pricing table: plan name,
   a large price, what you get as a checklist, and a full-width button pinned
   to the bottom so every button lines up across the row. One plan is
   featured with a lit silver edge, a tab on its top border and the only
   filled button. Silver, not a hue: the app's accent is the metal itself.
   Cards are a fixed dark ground on purpose, so they read the same at every
   canvas tone. The card is a <div> and the button is the only control, so a
   screen reader announces "Upgrade to Creator" instead of the whole card. */
#view-pricing .account-plan-grid,
#view-pricing .pricing-plans-section .account-plan-grid { gap: 26px 24px; margin-top: 30px; }
#view-pricing .account-plan-card {
  display: flex; flex-direction: column; text-align: left;
  padding: 32px 30px 30px; border-radius: 16px;
  background: #0e0f11; color: #e3e5e8;
  border: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: none; cursor: default;
}
#view-pricing .account-plan-card:hover { border-color: rgba(255, 255, 255, 0.18); }
#view-pricing .account-plan-card.is-featured {
  border-color: #d3d7dc;
  box-shadow: 0 0 0 1px rgba(211, 215, 220, 0.28), 0 22px 48px -22px rgba(0, 0, 0, 0.75);
}
#view-pricing .account-plan-card.is-current { border-color: rgba(255, 255, 255, 0.34); }

#view-pricing .account-plan-card-highlight {
  top: 0; right: auto; left: 50%; transform: translate(-50%, -50%);
  font-family: var(--font-mono); font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.14em;
  white-space: nowrap; padding: 5px 12px; border-radius: 999px;
  background: #0e0f11; color: #c9cdd2; border: 1px solid rgba(255, 255, 255, 0.22);
}
#view-pricing .account-plan-card.is-featured .account-plan-card-highlight {
  background: #e6e9ec; color: #0a0a0b; border-color: #e6e9ec;
}
#view-pricing .account-plan-card:has(.account-plan-card-highlight) .account-plan-card-name { padding-right: 0; }

#view-pricing .account-plan-card-name {
  font-family: var(--font-display); font-size: 0.9375rem; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase; color: #aeb3b9;
}
#view-pricing .account-plan-card.is-featured .account-plan-card-name { color: #f2f3f4; }
#view-pricing .account-plan-card-price {
  display: flex; align-items: baseline; gap: 6px; margin-top: 14px;
  font-family: var(--font-display); font-size: 2.75rem; font-weight: 800; line-height: 1;
  letter-spacing: -0.02em; color: #f7f7f8; font-variant-numeric: tabular-nums;
}
#view-pricing .account-plan-card-price-period {
  display: inline; margin: 0; font-family: var(--font-ui); font-size: 0.875rem; font-weight: 400;
  letter-spacing: 0; color: #8b9097;
}
#view-pricing .account-plan-card-billed { margin-top: 10px; font-size: 0.8125rem; color: #8b9097; }

#view-pricing .account-plan-card-features {
  list-style: none; margin: 26px 0 30px; padding: 0;
  display: flex; flex-direction: column; gap: 14px;
}
#view-pricing .account-plan-card-features li {
  display: flex; align-items: flex-start; gap: 11px;
  font-size: 0.90625rem; line-height: 1.4; color: #e3e5e8;
}
#view-pricing .plan-check { flex: 0 0 auto; width: 17px; height: 17px; margin-top: 1px; }
#view-pricing .plan-check circle { fill: #c8ccd1; }
#view-pricing .plan-check path { stroke: #0a0a0b; }

#view-pricing .account-plan-card-action,
#view-pricing .account-topup-card-action {
  display: flex; align-items: center; justify-content: center; width: 100%;
  margin-top: auto; min-height: 46px; padding: 0 16px; border-radius: 10px;
  font-family: var(--font-ui); font-size: 0.875rem; font-weight: 600;
  letter-spacing: 0; text-transform: none; cursor: pointer;
  background: transparent; color: #f2f3f4; border: 1px solid rgba(255, 255, 255, 0.18);
  transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
}
#view-pricing .account-plan-card-action:hover,
#view-pricing .account-topup-card-action:hover { background: rgba(255, 255, 255, 0.07); border-color: rgba(255, 255, 255, 0.32); }
#view-pricing .account-plan-card.is-featured .account-plan-card-action { background: #e9ecef; color: #0a0a0b; border-color: #e9ecef; }
#view-pricing .account-plan-card.is-featured .account-plan-card-action:hover { background: #ffffff; border-color: #ffffff; }
#view-pricing .account-plan-card-action:focus-visible,
#view-pricing .account-topup-card-action:focus-visible { outline: 2px solid #e9ecef; outline-offset: 3px; }
#view-pricing .account-plan-card-action.is-current,
#view-pricing .account-plan-card-action[disabled] {
  cursor: default; background: transparent; color: #8b9097; border-color: rgba(255, 255, 255, 0.12);
}

/* One-time packs: the same card language, one step smaller. */
#view-pricing .pricing-topup-section .account-topup-grid { gap: 20px; margin-top: 18px; }
#view-pricing .account-topup-card {
  display: flex; flex-direction: column; text-align: left;
  padding: 24px 24px 22px; border-radius: 14px;
  background: #0e0f11; color: #e3e5e8; border: 1px solid rgba(255, 255, 255, 0.1); cursor: default;
}
#view-pricing .account-topup-card:hover { border-color: rgba(255, 255, 255, 0.18); }
#view-pricing .pricing-topup-section .account-topup-card-name {
  font-family: var(--font-display); font-size: 0.8125rem; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase; color: #aeb3b9;
}
#view-pricing .pricing-topup-section .account-topup-card-price {
  display: flex; align-items: baseline; gap: 6px; margin-top: 10px;
  font-family: var(--font-display); font-size: 2rem; font-weight: 800; line-height: 1;
  letter-spacing: -0.02em; color: #f7f7f8; font-variant-numeric: tabular-nums;
}
#view-pricing .account-topup-card-price-period { font-family: var(--font-ui); font-size: 0.8125rem; font-weight: 400; letter-spacing: 0; color: #8b9097; }
#view-pricing .account-topup-card-note { margin: 10px 0 20px; font-size: 0.8125rem; line-height: 1.45; color: #8b9097; }

@media (max-width: 800px) {
  #view-pricing .account-plan-grid { gap: 30px; }
  #view-pricing .account-plan-card { padding: 30px 22px 24px; }
  #view-pricing .account-plan-card-price { font-size: 2.5rem; }
}
@media (prefers-reduced-motion: reduce) {
  #view-pricing .account-plan-card-action, #view-pricing .account-topup-card-action { transition: none; }
}

/* The section list on the right edge is fixed over the page above 1240px;
   keep the plan cards out from under it. */
@media (min-width: 1241px) {
  #view-pricing .pricing-canvas { padding-right: 240px; }
}
/* The refund and rollover note is a disclosure people must be able to read. */
#view-pricing .pricing-rollover-disclosure { font-size: 0.78125rem; line-height: 1.5; color: #9aa0a6; }
#view-pricing .pricing-rollover-disclosure a { color: #e3e5e8; }

/* Balance and current plan: the same dark card as the plans below. */
#view-pricing .pricing-status-strip {
  background: #0e0f11; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 16px; padding: 24px 30px;
}
#view-pricing .pricing-status-strip .section-label { color: #8b9097; }
#view-pricing .pricing-status-strip .account-balance-number,
#view-pricing .pricing-status-plan-name { color: #f7f7f8; }
#view-pricing .pricing-status-strip .settings-balance-sub,
#view-pricing .pricing-status-plan-price,
#view-pricing .pricing-status-refresh-note { color: #8b9097; }
#view-pricing .pricing-status-divider { background: rgba(255, 255, 255, 0.1); }
#view-pricing .pricing-status-strip .settings-manage-btn {
  min-height: 42px; padding: 0 18px; border-radius: 10px; background: transparent; color: #f2f3f4;
  border: 1px solid rgba(255, 255, 255, 0.18); font-weight: 600; letter-spacing: 0; text-transform: none; box-shadow: none;
}
#view-pricing .pricing-status-strip .settings-manage-btn:hover { background: rgba(255, 255, 255, 0.07); border-color: rgba(255, 255, 255, 0.32); }
