/* R29's three approved brand fonts (see _AGENCY/Standards/design.md) -
   Marlide Display via Adobe Fonts/Typekit (canonical block, confirmed
   2026-06-17), Inter and Roboto Mono via the Google Fonts <link> in
   index.html. Gloock is the approved web fallback if Adobe Fonts ever
   fails to load; Georgia/system stacks are the final fallback after that. */
@font-face {
  font-family: 'Marlide Display';
  src: url(https://use.typekit.net/af/26a9a4/0000000000000000774ba8d7/30/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3) format("woff2"),
       url(https://use.typekit.net/af/26a9a4/0000000000000000774ba8d7/30/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3) format("woff"),
       url(https://use.typekit.net/af/26a9a4/0000000000000000774ba8d7/30/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3) format("opentype");
  font-display: auto; font-style: normal; font-weight: 1 999; font-stretch: normal;
}

:root {
  --font-display: 'Marlide Display', 'Gloock', Georgia, serif;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-mono: 'Roboto Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Two light backgrounds only: --color-bg for chrome (rail, panel,
     inputs, cards), --color-canvas for the stage/work surface. A third,
     near-identical --color-panel used to exist here and was consolidated
     into --color-bg - it was close enough to be indistinguishable. */
  --color-bg: #ffffff;
  --color-canvas: #e8e8e8;
  --color-border: #e6e6e9;
  --color-text: #17181c;
  --color-text-muted: #7a7d85;
  --color-text-faint: #b0b2b8;
  --color-accent: #17181c;
  --color-accent-contrast: #ffffff;
  --color-danger: #b3261e;
  /* The literal logo color (see .brand-logo .logotype below) - constant
     across light/dark, never redefined in the dark-mode block, since a
     brand mark doesn't invert with the theme. */
  --color-brand: #ff7443;

  --radius-sm: 6px;
  --radius-md: 10px;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
}

/* Dark palette: rich near-black rather than flat #000 (bg/canvas/border
   keep a slight lift between them, the same way the light theme layers
   pure white against a light grey canvas), with greys doing the work
   anywhere a different tint is needed instead of pure black/white.
   Applies from either an explicit user choice - :root[data-theme="dark"],
   set by index.html's inline bootstrap script and the toggle in app.js -
   or, absent one, the OS setting via the media query; ":not([data-theme=
   light])" makes sure an explicit light choice always wins over the OS.
   Declared twice because a plain attribute selector and an @media block
   can't be merged into one rule when only one side should be gated by the
   media query - harmless duplication, not a maintenance trap, since both
   halves are edited together right here. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* bg is the lighter, forward surface (cards/rail/panel) and canvas the
       darker, receded one (the work surface behind them) - the same
       direction as light mode's white-card-on-grey-canvas, just inverted
       in absolute lightness. Previously bg was darker than canvas (the
       wrong direction) and the two sat only ~7 levels apart per channel -
       barely distinguishable. Widened the gap and fixed the direction. */
    --color-bg: #272a31;
    --color-canvas: #08090b;
    --color-border: #3d424b;
    --color-text: #f0f0f1;
    --color-text-muted: #9a9ca3;
    --color-text-faint: #5a5d64;
    --color-accent: #f0f0f1;
    --color-accent-contrast: #08090b;
  }
}
:root[data-theme="dark"] {
  --color-bg: #272a31;
  --color-canvas: #08090b;
  --color-border: #3d424b;
  --color-text: #f0f0f1;
  --color-text-muted: #9a9ca3;
  --color-text-faint: #5a5d64;
  --color-accent: #f0f0f1;
  --color-accent-contrast: #08090b;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}

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

.app {
  display: grid;
  /* Rail is wide enough for a properly-padded, label-width button (see
     .rail-item) to sit centered with real breathing room on the sides,
     not crammed edge to edge in the card. */
  grid-template-columns: 108px 320px 1fr;
  /* Row 1: the logo bar, sized to its content. Row 2: rail + panel,
     taking the rest - stage spans both rows, full height, unaffected. */
  grid-template-rows: auto 1fr;
  gap: 12px;
  height: 100vh;
  overflow: hidden;
  /* The shared canvas every floating card below (and the stage's own
     timeline card) sits on top of - see .brand-bar, .rail, .panel,
     .stage-footer. */
  background: var(--color-canvas);
}

/* Logo, full width of the rail+panel columns combined, sitting above
   both as its own card - see .rail and .panel below. */
.brand-bar {
  grid-column: 1 / 3;
  grid-row: 1;
  /* No bottom margin - the grid's own row-gap already separates this
     from .rail/.panel below, same reasoning as their own left/right
     margins relying on the column-gap rather than doubling it. */
  margin: 12px 0 0 12px;
  height: 50px;
  border-radius: 10px;
  background: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px;
  box-sizing: border-box;
}

.brand-logo {
  height: 26px;
  width: auto;
}
/* Wordmark follows the theme's text color (near-black in light mode, near-
   white in dark); the bracket mark keeps its brand orange in both. */
.brand-logo .logotype {
  fill: var(--color-text);
}
.brand-logo .mark {
  fill: #ff7443;
}

.theme-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex: none;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: none;
  cursor: pointer;
  color: var(--color-text-muted);
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.theme-toggle:hover {
  color: var(--color-text);
  background: var(--color-canvas);
  border-color: var(--color-border);
}
.theme-toggle svg { width: 18px; height: 18px; display: block; }

/* --- Icon rail --- */
.rail {
  grid-column: 1;
  grid-row: 2;
  /* A floating card, same treatment as .stage-footer: no border, just
     background contrast against the app's canvas, inset by margin. No
     top margin - the row-gap from .brand-bar above already provides it. */
  margin: 0 0 12px 12px;
  border-radius: 10px;
  background: var(--color-bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 12px;
  gap: var(--space-2);
  box-sizing: border-box;
  overflow-y: auto;
}

.rail-item {
  /* Sized to the widest label ("Controls", via .rail-label's fixed
     width below) so all four buttons match - the rail column is wide
     enough now (108px) for this to sit with real margin on the sides. */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: 10px;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: none;
  cursor: pointer;
  color: var(--color-text-muted);
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}

.rail-item svg { width: 20px; height: 20px; }

.rail-item:hover {
  color: var(--color-text);
  background: color-mix(in srgb, var(--color-canvas) 55%, transparent);
}
.rail-item:active { transform: scale(0.97); }

.rail-item.active {
  color: var(--color-text);
  border-color: var(--color-border);
  background: var(--color-canvas);
}

/* Fixed width, centered text - "Controls" is the widest label (~44px);
   giving every label the same footprint is what makes all four active/
   hover boxes the same size, regardless of how long each word is. */
.rail-item .rail-label { font-size: 11px; width: 46px; text-align: center; }

.rail-status {
  margin-top: auto;
  width: 72px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 9.5px;
  line-height: 1.45;
  letter-spacing: 0.02em;
  color: var(--color-text-faint);
}
.rail-status.warn { color: var(--color-danger); }

/* --- Settings panel --- */
.panel {
  grid-column: 2;
  grid-row: 2;
  /* A floating card too - left/right gaps come from the grid's own gap.
     No top margin - the row-gap from .brand-bar above provides it. */
  margin: 0 0 12px 0;
  border-radius: 10px;
  padding: 12px;
  overflow-y: auto;
  background: var(--color-bg);
  box-sizing: border-box;
}

.panel-section {
  padding-bottom: var(--space-5);
  margin-bottom: var(--space-5);
  border-bottom: 1px solid var(--color-border);
}
.panel-section:last-child { border-bottom: none; }

.panel-section h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 21px;
  margin: 0 0 var(--space-4);
}

.field { margin-bottom: var(--space-4); }
.field:last-child { margin-bottom: 0; }

.field-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 12px;
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.field-value {
  font-variant-numeric: tabular-nums;
  text-transform: none;
  letter-spacing: 0;
  color: var(--color-text);
}

.mono { font-family: var(--font-mono); font-size: 12px; }

/* Aspect ratio: orientation swap + compact ratio row */
.ratio-row {
  display: flex;
  gap: var(--space-2);
  align-items: stretch;
}
.swap-btn {
  width: 34px;
  flex: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  color: var(--color-text-muted);
}
.swap-btn.active {
  background: var(--color-accent);
  color: var(--color-accent-contrast);
  border-color: var(--color-accent);
}
.ratio-row .segmented { flex: 1; }

/* Condensed slider: label to the left, one wide draggable track to the
   right that is both the value display and the input - no separate
   range + number-input row. See fieldSlider in app.js. */
.field-track-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.field-track-label {
  flex: none;
  width: 92px;
  font-size: 12px;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.track-wrap { flex: 1; position: relative; }
.track-control {
  position: relative;
  height: 30px;
  border-radius: var(--radius-md);
  background: var(--color-canvas);
  overflow: hidden;
  /* The native left-right resize cursor - a short bar with arrows on both
     ends - reads immediately as "drag this side to side," which is exactly
     what this control does. No custom cursor image needed. */
  cursor: ew-resize;
  touch-action: none;
  user-select: none;
}
.track-fill {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  background: var(--color-accent);
  pointer-events: none;
}
.track-value {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.track-value-on { color: var(--color-accent-contrast); }
.track-value-off { color: var(--color-text); }
.track-edit-input {
  width: 100%;
  height: 30px;
  padding: 0 var(--space-3);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  text-align: center;
  font-size: 12px;
}

/* Snap's Custom per slot list - a condensed, single-line control per slot:
   label, a thin drag-to-set track, and an always-editable number box to the
   right, rather than fieldSlider's wide clipped-text track (fine for one
   control, too tall for up to 20 in a row). See fieldSlotRow in app.js. */
.slotrow-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-3);
}
.slotrow {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.slotrow-label {
  flex: none;
  width: 52px;
  font-size: 12px;
  color: var(--color-text-muted);
}
.slotrow-track {
  position: relative;
  flex: 1;
  height: 6px;
  border-radius: 3px;
  background: var(--color-canvas);
  cursor: ew-resize;
  touch-action: none;
  user-select: none;
}
.slotrow-fill {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  border-radius: 3px;
  background: var(--color-accent);
  pointer-events: none;
}
/* A small round handle at the fill's leading edge - the track alone reads
   as a plain progress bar; this is what makes it read as draggable. */
.slotrow-fill::after {
  content: '';
  position: absolute;
  top: 50%;
  right: -5px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--color-accent);
  transform: translateY(-50%);
}
.slotrow-input {
  flex: none;
  width: 56px;
  height: 26px;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  color: var(--color-text);
  text-align: center;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

input[type="range"] {
  flex: 1;
  -webkit-appearance: none;
  height: 4px;
  border-radius: 2px;
  background: var(--color-border);
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--color-accent);
  cursor: pointer;
}

.number-input {
  width: 64px;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  text-align: right;
}

.segmented {
  position: relative;
  display: flex;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  /* Selectable by press-and-slide as well as a plain click - see
     segmentedControl's pointerdown/pointermove handling in app.js. */
  touch-action: none;
  user-select: none;
}
/* The sliding active-state pill - a single persistent element positioned
   by percentage (index/count) so its transform can transition smoothly
   between options instead of snapping. See fieldSegmented in app.js. */
.segmented-thumb {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  background: var(--color-accent);
  transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: none;
  z-index: 0;
}
.segmented button {
  position: relative;
  z-index: 1;
  flex: 1;
  padding: var(--space-2) var(--space-2);
  border: none;
  background: transparent;
  cursor: pointer;
  color: var(--color-text-muted);
  font-size: 12px;
  transition: color 0.22s ease;
}
.segmented.compact button { padding: var(--space-2) 0; font-size: 11px; }
.segmented button.active {
  color: var(--color-accent-contrast);
}

/* Inset variant (see segmentedControl's `inset` option in app.js): the pill
   floats a few pixels inside the frame on every side instead of filling it
   edge to edge, and eases across with a slower, symmetric curve (equal
   acceleration in and out) so a two-way toggle reads as one fluid glide
   rather than the snappier flip used for ratio/order pickers. */
.segmented.inset .segmented-thumb {
  top: 3px;
  bottom: 3px;
  border-radius: calc(var(--radius-md) - 3px);
  transition: transform 0.32s cubic-bezier(0.65, 0, 0.35, 1);
}

/* Aspect-ratio buttons: a shape icon scaled to the actual ratio, stacked
   above the label - shows what the frame looks like, not just its name. */
.ratio-row .segmented button {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: var(--space-2) 2px;
}
.ratio-row .segmented button svg { display: block; }
.ratio-row .segmented button.active svg { color: var(--color-accent-contrast); }

.color-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* Custom color picker - replaces the native <input type="color">, whose
   drag behaviour is rendered by the OS and isn't something this codebase
   can fix if it misbehaves for a given browser/user. */
.color-picker { position: relative; }

.color-swatch {
  width: 36px;
  height: 36px;
  /* An empty button (no text) has near-zero intrinsic min-width, so
     without this it's the first thing squeezed to nothing the moment a
     flex sibling (e.g. the Colors/Swipe row's own Name field) needs room -
     the swatch should never shrink below its own fixed size, in any row. */
  flex-shrink: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0;
  cursor: pointer;
  /* The swatch's own background IS the full color - no inset native
     rendering, so no "square inside a square" artifact. */
}

/* Typing an exact hex value directly, alongside the swatch/popover - an
   invalid or partial value just reverts to the last valid color on blur
   rather than blocking typing, since a 6-digit hex is only ever "done"
   once every character is in. */
.hex-input {
  width: 78px;
  flex-shrink: 0; /* same reasoning as .color-swatch above */
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  text-transform: uppercase;
}

/* Samples a color from anywhere on screen via the browser's own EyeDropper
   API - see colorPicker in app.js. Sits in the RGB row, to the left of the
   R/G/B fields - align-self overrides that row's default stretch (the R/G/B
   fields are taller, input+label) so this stays its own size, top-aligned
   with the number inputs rather than centered against the labels below them. */
.eyedropper-btn {
  flex: none;
  align-self: flex-start;
  width: 30px;
  height: 30px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  color: var(--color-text-muted);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.eyedropper-btn:disabled { opacity: 0.35; cursor: not-allowed; }

.color-popover {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: 20;
  /* As wide as the panel comfortably allows (panel content is 296px after
     its own 12px padding) - opening to the side instead was considered,
     but the panel's own overflow-y:auto clips overflow-x too, and the
     space to the swatch's right is a much narrower slice of the row than
     the full width available below it. */
  width: 280px;
  padding: var(--space-3);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}

.sv-wrap {
  position: relative;
  width: 100%;
  height: 130px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  margin-bottom: var(--space-3);
  cursor: crosshair;
  touch-action: none;
}
.sv-wrap canvas { display: block; width: 100%; height: 100%; }
.sv-marker {
  position: absolute;
  width: 14px;
  height: 14px;
  margin-left: -7px;
  margin-top: -7px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.4);
  pointer-events: none;
}

.hue-range {
  display: block;
  width: 100%;
  height: 14px;
  -webkit-appearance: none;
  border-radius: 7px;
  margin-bottom: var(--space-3);
  background: linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000);
}
.hue-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  border: 2px solid var(--color-border);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  cursor: pointer;
}

.rgb-row { display: flex; gap: var(--space-2); }
.rgb-field {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.rgb-field input {
  width: 100%;
  text-align: center;
  padding: var(--space-1);
}
.rgb-field span {
  font-size: 10px;
  color: var(--color-text-faint);
  text-transform: uppercase;
}

/* Recent-colors history inside the color popover - see colorPicker's own
   renderHistory. Collapses (hidden) entirely when there's no history yet. */
.color-history-section {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}
.color-history-section .field-label { margin-bottom: var(--space-2); }
.color-history-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.color-history-chip {
  width: 22px;
  height: 22px;
  flex: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0;
  cursor: pointer;
}
.color-history-chip:hover { transform: scale(1.1); }

.hint {
  font-size: 11px;
  color: var(--color-text-faint);
  margin-top: var(--space-2);
  line-height: 1.5;
}

/* Presets panel: a 3-column picker of animation styles, each a small
   illustrative thumbnail (see presetThumbSvg/gridPresetThumb in app.js) -
   visual so the difference between a grid and a filmstrip reads at a
   glance. 4:5 (matching the app's own default export ratio) reads as
   slimmer/taller than a landscape thumbnail would, which is what makes 3
   columns fit the panel's width better than 2. */
.preset-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: var(--space-3);
}
.preset-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  /* Tighter than the space-3 used elsewhere - 3 narrow columns can't
     afford to spend as much of their width on padding as 2 wider ones
     could. */
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  cursor: pointer;
}
.preset-card:hover { border-color: var(--color-text-faint); }
.preset-card.active {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
  background: var(--color-canvas);
}
.preset-thumb {
  width: 100%;
  aspect-ratio: 4 / 5;
  background: var(--color-canvas);
  border-radius: var(--radius-sm);
  color: var(--color-text-faint);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  /* No inner padding - the thumb box's own edge IS the frame, so the
     animation/icon should touch it directly rather than sit inset with a
     ring of the box's own background showing as a buffer around it. */
  box-sizing: border-box;
}
.preset-card.active .preset-thumb { color: var(--color-accent); background: var(--color-bg); }
.preset-thumb svg { width: 100%; height: 100%; }
.preset-name { font-size: 12px; color: var(--color-text); }

/* Number-of-slots stepper: a +/- pair either side of the count, replacing
   a free-typed number input - the count only ever needs nudging by one,
   never typing an arbitrary value. */
.stepper {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.stepper-btn {
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  color: var(--color-text);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.stepper-btn:hover { border-color: var(--color-text-faint); }
.stepper-btn:disabled { opacity: 0.35; cursor: default; }
.stepper-value {
  min-width: 20px;
  text-align: center;
  font-size: 13px;
  color: var(--color-text);
}
.btn {
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  cursor: pointer;
}
.btn.primary {
  background: var(--color-brand);
  color: #000000;
  border-color: var(--color-brand);
  transition: background-color 0.15s ease, color 0.15s ease;
}
.btn.primary:hover:not(:disabled) {
  background: #000000;
  color: #ffffff;
  border-color: #000000;
}
.btn.full { width: 100%; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }

/* --- Center stage --- */
.stage {
  grid-column: 3;
  /* Spans both rows - full height, unaffected by the logo bar carved
     out of the rail+panel columns to its left. */
  grid-row: 1 / 3;
  display: flex;
  flex-direction: column;
  background: var(--color-canvas);
}

.stage-body {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
  overflow: auto;
}

/* Sizes to the canvas's own width (not the stage's) and left-aligns the
   caption beneath it - see renderPreviewStage in app.js. */
.preview-stack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* No mat board, no padding, no rounding: the exported frame is a hard
   rectangle, so the preview has to be one too. Rounding belongs to the
   cards inside, which the engine draws. A soft shadow is the only chrome,
   there to keep a light background colour from bleeding into the stage. */
.preview-frame {
  line-height: 0;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12);
}

/* A quiet caption, not a label competing with the preview - see renderAll
   in app.js. Canvas-only chrome: never drawn into the exported video. */
.preview-preset-label {
  margin-top: 10px;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--color-text-muted);
}

/* Sized in JS to fit the stage at device resolution - see drawPreviewFrame. */
#previewCanvas {
  display: block;
}

.stage-footer {
  /* A floating white card, not a bar flush against the stage - the stage
     background is a mid gray now (E8E8E8) and a flush, borderless bar in
     that same tone read as invisible. */
  background: var(--color-bg);
  border-radius: 10px;
  /* No left margin - the grid's own 12px gap between panel and stage
     already provides that inset, same as the panel's own left/right
     margins rely on the grid gap rather than doubling it. */
  margin: 0 12px 12px 0;
  padding: 12px;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--color-text-muted);
  font-size: 12px;
}
.stage-footer .transport-btn {
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-text);
  border-radius: var(--radius-sm);
  width: 30px;
  height: 30px;
  flex: none;
  padding: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
}
.stage-footer .transport-btn svg { width: 14px; height: 14px; display: block; }
.stage-footer .transport-btn:hover:not(:disabled) {
  background: var(--color-canvas);
  border-color: var(--color-text-faint);
}
.stage-footer .transport-btn:active:not(:disabled) { transform: scale(0.94); }
.stage-footer .transport-btn:disabled { opacity: 0.4; cursor: not-allowed; }

/* One button, two glyphs - which one shows follows playback state. */
#playToggle .icon-pause { display: none; }
#playToggle.is-playing .icon-play { display: none; }
#playToggle.is-playing .icon-pause { display: block; }

/* Timeline: a tall, invisible hit area (easy to grab) with a slim visible
   track inside it, a filled "played" portion (--scrub-pct, set from JS),
   and a thumb that grows on hover/drag. */
#scrubber {
  flex: 1;
  min-width: 0;
  -webkit-appearance: none;
  appearance: none;
  height: 22px;
  margin: 0;
  background: transparent;
  cursor: pointer;
  touch-action: none;
  --scrub-pct: 0%;
}
#scrubber:disabled { cursor: not-allowed; opacity: 0.5; }
#scrubber::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(to right,
    var(--color-accent) 0, var(--color-accent) var(--scrub-pct),
    var(--color-border) var(--scrub-pct), var(--color-border) 100%);
}
#scrubber::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--color-border);
}
#scrubber::-moz-range-progress {
  height: 4px;
  border-radius: 2px;
  background: var(--color-accent);
}
#scrubber::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -5px;
  border-radius: 50%;
  border: 2px solid var(--color-bg);
  background: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
  cursor: grab;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
#scrubber::-moz-range-thumb {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid var(--color-bg);
  background: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
  cursor: grab;
}
#scrubber:hover:not(:disabled)::-webkit-slider-thumb { transform: scale(1.2); }
#scrubber:active:not(:disabled)::-webkit-slider-thumb {
  transform: scale(1.3);
  cursor: grabbing;
  box-shadow: 0 0 0 1px var(--color-accent), 0 0 0 6px color-mix(in srgb, var(--color-accent) 16%, transparent);
}
#scrubber:focus-visible { outline: none; }
#scrubber:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 1px var(--color-accent), 0 0 0 5px color-mix(in srgb, var(--color-accent) 28%, transparent);
}

#timeReadout {
  flex: none;
  /* A true fixed width, not min-width - min-width is only a floor, so once
     the seconds count crossed into double digits ("9.9s" -> "10.0s") the
     text grew past it and widened this element, stealing pixels from the
     scrubber's flex:1 share. That shrank the track by a couple of pixels
     right at that moment, which is what made the playhead look like it
     skipped - its % position didn't change, but the track it's a % of did.
     Sized generously (three-digit seconds on both sides) so it never has
     to grow again; tabular-nums keeps each digit's width identical too, so
     nothing shifts frame to frame even before hitting double digits. */
  width: 104px;
  overflow: hidden;
  white-space: nowrap;
  text-align: right;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}

/* --- Media slot list (Cards panel, left sidebar) --- */
.slot-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-height: 46vh;
  overflow-y: auto;
  margin-bottom: var(--space-3);
}

.slot-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  cursor: pointer;
}
.slot-row.filled { border-style: solid; }
.slot-row.dragging { opacity: 0.4; }
.slot-row.drag-over { border-color: var(--color-accent); border-style: dashed; }
.slot-row:hover { border-color: var(--color-text-faint); }
/* Selected for preview - see selectSlot in app.js. A clear ring, not just
   a border tweak, since this needs to read as "this is what's on the
   artboard right now," not a hover-adjacent state. */
.slot-row.selected {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
  background: var(--color-canvas);
}

.slot-handle {
  flex: none;
  color: var(--color-text-faint);
  cursor: grab;
  display: flex;
  align-items: center;
}

.slot-thumb {
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--color-canvas);
  display: flex;
  align-items: center;
  justify-content: center;
}
.slot-thumb img, .slot-thumb video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.slot-thumb-plus {
  color: var(--color-text-faint);
  font-size: 16px;
}

.slot-label {
  flex: 1;
  min-width: 0;
}
.slot-title {
  font-size: 12px;
  color: var(--color-text);
}
.slot-sub {
  font-size: 11px;
  color: var(--color-text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- Colors panel (see renderColorsPanel) --- */
.color-row-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
}
/* The swatch+hex pair inside never shrinks (see their own flex-shrink:0) -
   so this shouldn't either, or its content just overflows past its own box
   and visually overlaps whatever sits next to it (the Name field). Fixed
   size, never grows or shrinks: the Name field (flex:1 below) is the one
   piece of this row actually meant to give up space when it's tight. */
.color-row-item .color-picker { flex: none; }

.color-name-input {
  flex: 1;
  /* A real floor, not 0 - an input box this narrow still reads as "a
     field," rather than vanishing the instant space is tight. Native text
     inputs already clip their own overflowing text with no extra CSS
     needed (unlike a plain div, text-overflow doesn't apply here). */
  min-width: 40px;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
}

.primary-toggle {
  flex: none;
  font-size: 11px;
  padding: 5px 10px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: none;
  color: var(--color-text-muted);
  cursor: pointer;
  white-space: nowrap;
}
.primary-toggle.active {
  border-color: var(--color-accent);
  background: var(--color-accent);
  color: var(--color-accent-contrast);
}

.color-row-remove {
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-sm);
  border: none;
  background: none;
  color: var(--color-text-faint);
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
}
.color-row-remove:hover { color: var(--color-danger); }
.color-row-remove:disabled { opacity: 0.3; cursor: default; }

/* --- Export panel --- */
.export-status {
  font-size: 12px;
  line-height: 1.6;
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--color-canvas);
  margin-top: var(--space-3);
}
.export-status.error { color: var(--color-danger); }
.export-status.success a { color: var(--color-text); }

.export-progress {
  height: 4px;
  border-radius: 2px;
  background: var(--color-border);
  overflow: hidden;
  margin-top: var(--space-2);
}
.export-progress-fill {
  height: 100%;
  width: 0%;
  background: var(--color-accent);
  transition: width 0.1s linear;
}

.ffmpeg-banner {
  border: 1px solid var(--color-border);
  background: var(--color-canvas);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  font-size: 12px;
  line-height: 1.6;
  margin-bottom: var(--space-4);
}
.ffmpeg-banner code {
  display: block;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  margin-top: var(--space-1);
  overflow-x: auto;
}


/* --- Keyboard focus + motion preferences --- */
button:focus-visible,
.rail-item:focus-visible,
.theme-toggle:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--color-accent) 55%, transparent);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* --- Responsive ---
   Desktop is the real target; these just keep tablet and phone from
   breaking. Under 1100px the columns tighten; under 860px the layout
   stacks: logo, then a sticky preview + timeline, then the section tabs as
   a horizontal strip, then the settings panel, with the page itself
   scrolling instead of each card. */
@media (max-width: 1100px) {
  .app { grid-template-columns: 92px 280px 1fr; gap: 10px; }
  .rail-item .rail-label { width: 42px; font-size: 10.5px; }
  .stage-body { padding: var(--space-4); }
}

@media (max-width: 860px) {
  html, body { height: auto; }
  .app {
    display: flex;
    flex-direction: column;
    height: auto;
    min-height: 100vh;
    overflow: visible;
    gap: 10px;
    padding-bottom: 12px;
  }
  .brand-bar { margin: 10px 12px 0; height: 46px; order: 1; }

  .stage {
    order: 2;
    position: sticky;
    top: 0;
    z-index: 10;
    background: var(--color-canvas);
    padding-top: 2px;
  }
  .stage-body { flex: none; height: 42vh; min-height: 240px; padding: var(--space-3); }
  .stage-footer { margin: 0 12px 4px; padding: 10px; gap: var(--space-2); }
  #timeReadout { width: 88px; }

  .rail {
    order: 3;
    margin: 0 12px;
    flex-direction: row;
    justify-content: space-between;
    gap: var(--space-1);
    padding: 6px;
    overflow-x: auto;
    overflow-y: hidden;
  }
  .rail-item { flex: 1; min-width: 56px; padding: 8px 4px; }
  .rail-item .rail-label { width: auto; font-size: 10.5px; }
  .rail-status { display: none; }

  .panel {
    order: 4;
    margin: 0 12px;
    overflow: visible;
  }
  .slot-list { max-height: none; }
  /* Thumbnails scale with their column; without a cap they balloon once the
     panel spans the whole screen. */
  .preset-grid { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
}

@media (max-width: 480px) {
  .brand-logo { height: 22px; }
  .stage-body { height: 36vh; min-height: 200px; }
  .field-track-label { width: 76px; }
}

/* --- Cadence the cat ---
   Fixed along the bottom edge, above everything, never intercepting clicks;
   only exists visually while an export is rendering. Position is set from
   JS (see setExportCatPosition in app.js). */
.export-cat {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 180px; /* overridden in JS: scales down on smaller windows */
  overflow: hidden;
  pointer-events: none;
  z-index: 2000;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.export-cat.is-visible { opacity: 1; }
.export-cat-rider {
  position: absolute;
  left: 0;
  transition: transform 0.12s linear;
  will-change: transform;
}
.export-cat-rider svg { display: block; width: 100%; height: 100%; }
