/*
  Studio: the preview, the transport, and the timeline under them.

  The timeline is the page. Everything on it is positioned at seconds × zoom,
  which is the same arithmetic the renderer uses, so a piece twice as wide as
  another really does last twice as long — and a gap between two of them is a
  gap in the film rather than a margin.
*/

.studio {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/*
  The project list.

  A timeline has no name anyone remembers and every project card is otherwise
  the same shape, so the thing that makes this list usable is a picture: one
  frame off the front of each timeline, at the frame's own aspect ratio. Nine
  identical rows of text become nine recognisably different films.

  The newest project takes the top of the page on its own, because editing video
  is not finished in one sitting and the one anybody wants is nearly always the
  one they had open last.
*/
.studio-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--sp-4);
}

.studio-card {
  position: relative;
  display: grid;
  min-width: 0;
}

.studio-open {
  display: flex;
  flex-direction: column;
  gap: 0;
  /* Without this a flex or grid item refuses to go narrower than its widest
     line of text, and every card in the grid grows past its column and sits on
     top of the next one. */
  min-width: 0;
  padding: 0;
  overflow: hidden;
  text-align: left;
  background: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  cursor: pointer;
  transition: border-color var(--t-fast), transform var(--t-fast),
    background var(--t-fast);
}

.studio-open:hover {
  background: var(--s3);
  border-color: var(--border-2);
  transform: translateY(-2px);
}

/* The frame. Black behind the picture rather than the card colour, so an
   upright or square film reads as pillarboxed footage instead of as a
   thumbnail that failed to fill its box. */
.studio-frame {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: #05060a;
  border-bottom: 1px solid var(--border);
}

.studio-shot {
  width: 100%;
  height: 100%;
  /* Contained, because the shape of the frame is information on this page: it
     is the shape every piece in the project has to be. */
  object-fit: contain;
  opacity: 1;
  transition: opacity var(--t-med);
}

.studio-shot.loading {
  opacity: 0;
}

.studio-blank {
  color: var(--fg-3);
}

.studio-blank svg {
  width: 30px;
  height: 30px;
}

.studio-len {
  position: absolute;
  right: 8px;
  bottom: 8px;
  padding: 2px 7px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: #fff;
  background: rgb(0 0 0 / 66%);
  border-radius: var(--r-sm);
  backdrop-filter: blur(2px);
}

/* Top left: the discard owns the right corner and the running time the bottom. */
.studio-flag {
  position: absolute;
  top: 8px;
  left: 8px;
}

.studio-said {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: var(--sp-3) var(--sp-4);
}

.studio-said h3 {
  margin: 0;
}

.studio-said p {
  margin: 0;
}

/* --------------------------------------------------------------- the hero */

.studio-hero {
  margin-bottom: var(--sp-5);
}

.studio-hero .studio-open {
  flex-direction: row;
  align-items: stretch;
}

.studio-hero .studio-frame {
  flex: none;
  width: min(46%, 520px);
  border-right: 1px solid var(--border);
  border-bottom: 0;
}

.studio-hero .studio-said {
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-5);
}

.studio-hero .studio-said h3 {
  font-size: 24px;
}

/* A button in look only: the whole card is the click target, and a real one
   inside it is a button nested in a button. */
.studio-go {
  align-self: flex-start;
  margin-top: var(--sp-2);
  pointer-events: none;
}

@media (max-width: 700px) {
  .studio-hero .studio-open {
    flex-direction: column;
  }

  .studio-hero .studio-frame {
    width: 100%;
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }

  .studio-hero .studio-said {
    padding: var(--sp-4);
  }
}

/*
  The discard, over the frame's corner.

  Kept faint until the card is hovered, so a list of projects reads as projects
  rather than as a row of delete buttons. Touch has no hover, so there it is
  simply always there.
*/
.studio-card-x {
  position: absolute;
  top: 8px;
  right: 8px;
  background: rgb(0 0 0 / 55%);
  opacity: 0;
  transition: opacity var(--t-fast);
}

.studio-card:hover .studio-card-x,
.studio-card-x:focus-visible {
  opacity: 1;
}

@media (hover: none) {
  .studio-card-x {
    opacity: 1;
  }
}

/* Every other page reads better with a measure; this one reads better with
   room. The timeline is drawn at pixels per second, so width is precision. */
.page-body.wide {
  max-width: none;
}

.studio-top {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  min-width: 0;
}

.studio-stage {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  min-width: 0;
}

/* The editing buttons, grouped so a narrow head can hand them a row of their
   own. Pushed to the right of the title on a wide one, where they sit in the
   same order they always did. */
.studio-acts {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  margin-left: auto;
}

/* The controls are a column on the page at this width, so there is nothing for
   a button that opens them to open. */
.studio-props {
  display: none;
}

.studio-side {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* Only ever a drawer's head, and a drawer only happens on a narrow screen. */
.studio-side-head,
.studio-side-back {
  display: none;
}

/*
  The phone layout: two rows of head, then the picture, then the timeline.

  Kept in step with `drawer` in js/views/studio.js, which asks the same question
  at the same width to decide whether opening the panel means anything.
*/
@media (max-width: 860px) {
  /*
    The title shares the first row instead of taking one.

    An <input> is `width: 100%` by default — see css/tokens.css — which as a flex
    base size is the whole head, so it wrapped onto a row of its own and left the
    back arrow alone above it. A small basis it can grow from puts it back beside
    the arrow, with Render and the overflow menu on the end.
  */
  .studio-head .studio-title {
    flex: 1 1 90px;
    width: auto;
    max-width: none;
  }

  /*
    The editing buttons on a row of their own, scrolled sideways rather than
    wrapped.

    Seven labelled buttons are about a screen and a half. Wrapped, they are two
    more rows of head on top of the one above, and the head is already the first
    thing between you and the timeline; swiped, they are one row and keep their
    words, which is worth more here than on a desktop — a phone has no tooltips,
    so an icon on its own is unreadable.
  */
  .studio-acts {
    order: 1;
    flex: 1 0 100%;
    margin-left: 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    /* Room for the focus ring, which a clipped overflow would cut in half. */
    padding: 2px;
  }

  .studio-acts::-webkit-scrollbar {
    display: none;
  }

  /* Their labels, not a share of a row that was never wide enough. */
  .studio-acts > * {
    flex: none;
  }

  .studio-props {
    display: inline-flex;
  }

  /*
    The controls as a drawer over the page.

    On the page they sat between the picture and the timeline: four panels of
    things nobody had asked about, in front of the one thing the page is for. And
    the inspector is the tallest of them when a piece is selected, so touching a
    piece pushed the timeline you were editing off the bottom of the screen.

    Over the page instead, and only when asked for — from the Properties button
    or a piece's own menu. Which leaves the timeline directly under the picture,
    where a phone has room for it.
  */
  .studio-side {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 86;
    width: min(420px, 100%);
    background: var(--s1);
    border-left: 1px solid var(--border);
    box-shadow: var(--shadow-3);
    /* Past its own width, so the shadow clears the edge with it. */
    transform: translateX(101%);
    visibility: hidden;
    /* Visibility so it is not a tab stop while it is off the screen, and in the
       transition so it lasts the slide out rather than vanishing at the start. */
    transition: transform var(--t-fast), visibility var(--t-fast);
  }

  .studio-side.open {
    transform: none;
    visibility: visible;
  }

  .studio-side-head {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-4);
    border-bottom: 1px solid var(--border);
  }

  .studio-side-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--sp-4);
    padding-bottom: calc(var(--sp-5) + env(safe-area-inset-bottom));
  }

  /* Tapping the page shuts it, which is the gesture people try first. Back does
     it too — see `openSide`. */
  .studio-side-back:not([hidden]) {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 85;
    background: rgb(6 7 10 / 60%);
  }
}

/*
  The column's contents, in a box that can be taken out of the flow.

  Stacked normally here, where the column is under the picture and growing down
  the page is the right thing to do. On a wide screen it becomes a scroller — see
  the row rule below, which is where the reason lives.
*/
.studio-side-scroll {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
}

/*
  The picture is a box of the film's shape, and the players fill it.

  Not the other way round, which is what this was: a <video> sized by whatever it
  had loaded meant the stage resized every time a player was handed a different
  file — and while a src was in flight, an element holding nothing falls back to
  300×150 and the whole page jumped. Scrubbing across a cut did that twice.
  Shape belongs to the film, which is a fact about the project and does not
  change while anyone is scrubbing.
*/
.studio-screen {
  position: relative;
  width: min(100%, calc(52vh * var(--fw, 16) / var(--fh, 9)));
  margin-inline: auto;
  aspect-ratio: var(--fw, 16) / var(--fh, 9);
  border-radius: var(--r-md);
  background: #000;
  overflow: hidden;
}

.studio-preview {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  background: #000;
  object-fit: contain;
}

/*
  The layers, drawn over the picture.

  The sheet itself is the letterboxed picture area rather than the whole stage —
  the renderer's coordinates are fractions of the frame, and on a stage that is
  taller than the film those two are not the same box. Sized in script, because
  only the script knows the film's shape.
*/
.studio-layers {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.studio-layer {
  position: absolute;
  border: 1px dashed rgb(255 255 255 / 45%);
  pointer-events: auto;
  cursor: grab;
  touch-action: none;
}

.studio-layer:hover {
  border-color: rgb(255 255 255 / 80%);
}

.studio-layer.on {
  border: 1px solid var(--accent);
  box-shadow: 0 0 0 1px rgb(0 0 0 / 50%);
}

.studio-layer.dragging {
  cursor: grabbing;
}

/* The layer's own picture, so what is dragged looks like what is composited
   rather than like an empty rectangle with a label. */
.studio-layer-art {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: fill;
  pointer-events: none;
  user-select: none;
}

/* Only the selected layer wears its handles: four corner boxes on every layer
   would bury the picture the moment there are three of them. */
.studio-grip {
  position: absolute;
  width: 13px;
  height: 13px;
  border: 1px solid rgb(0 0 0 / 60%);
  border-radius: 3px;
  background: var(--accent);
  cursor: nwse-resize;
  touch-action: none;
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease);
}

.studio-layer.on > .studio-grip {
  opacity: 1;
}

.studio-grip.nw { top: -7px; left: -7px; }
.studio-grip.ne { top: -7px; right: -7px; cursor: nesw-resize; }
.studio-grip.se { bottom: -7px; right: -7px; }
.studio-grip.sw { bottom: -7px; left: -7px; cursor: nesw-resize; }

/* Above the top edge, on a stalk, which is where every editor puts it and so
   is where the hand goes without being told. */
.studio-grip.turn {
  top: -26px;
  left: 50%;
  margin-left: -7px;
  border-radius: 50%;
  background: var(--bg-1);
  cursor: grab;
}

.studio-grip.turn::before {
  content: "";
  position: absolute;
  top: 12px;
  left: 5px;
  width: 1px;
  height: 14px;
  background: var(--accent);
}

.studio-guide {
  position: absolute;
  background: var(--accent);
  opacity: 0.7;
  pointer-events: none;
}

.studio-guide.v {
  top: 0;
  bottom: 0;
  width: 1px;
}

.studio-guide.h {
  left: 0;
  right: 0;
  height: 1px;
}

.studio-place,
.studio-text {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}

.studio-place input[type="range"],
.studio-text input[type="range"] {
  width: 118px;
}

/*
  The marquee: a sheet that takes the pointer while it is armed, and the
  rectangle it draws.

  The sheet is above the layer boxes on purpose. Without it, dragging a
  selection that happens to start on a title would move the title, which is the
  one thing a selection tool must never do.
*/
.studio-marquee-sheet {
  position: absolute;
  inset: 0;
  z-index: 3;
  cursor: crosshair;
  touch-action: none;
}

.studio-marquee {
  position: absolute;
  z-index: 2;
  border: 1px dashed #fff;
  background: rgb(255 255 255 / 8%);
  box-shadow: 0 0 0 1px rgb(0 0 0 / 55%) inset;
  pointer-events: none;
}

/* Armed, so the picture says so even where the pointer is not. */
.studio-screen.marquee-on {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The sample under the marquee, in the fill sheet. */
.fill-sample {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding-block: var(--sp-2);
  border-bottom: 1px solid var(--line);
}

.fill-shot {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}

.fill-shot img {
  width: 108px;
  max-height: 108px;
  object-fit: contain;
  border-radius: var(--r-sm);
  background: #000;
}

.studio-keys {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: 1px dashed var(--line);
}

.studio-key {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  padding: 2px var(--sp-1);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
}

/* The stop the playhead is on, because the sliders above are editing it and
   which one that is has to be obvious without counting. */
.studio-key.on {
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.studio-key select {
  max-width: 116px;
}

.studio-words {
  width: 100%;
  min-height: 3.2em;
  padding: var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--bg-2);
  color: var(--fg);
  font: inherit;
  line-height: 1.35;
  resize: vertical;
}

/* A native colour input is a chrome-heavy button on every platform; stripped
   back it reads as a swatch, which is what it is. */
.studio-swatch {
  width: 34px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: none;
  cursor: pointer;
}

.studio-swatch::-webkit-color-swatch-wrapper {
  padding: 2px;
}

.studio-swatch::-webkit-color-swatch {
  border: none;
  border-radius: 2px;
}

/* A title's block carries no filmstrip, so the words in the label are all there
   is to recognise it by — worth a little more room and a hint of the accent. */
.tl-piece.text {
  background: color-mix(in srgb, var(--accent) 22%, var(--bg-2));
}

.tl-piece.text .tl-name {
  font-weight: 600;
}

/*
  With room to spare, the monitor and the controls sit beside each other the way
  they do in an editor — which is not symmetry for its own sake: it buys back the
  vertical space the inspector was taking from the timeline, and the timeline is
  the thing that always wants more. The picture keeps most of the width because
  it is what is being judged; the column is sized in ch so the controls in it
  stay a readable line length however wide the screen gets.
*/
@media (width >= 1280px) {
  .studio-top {
    flex-direction: row;
    /* Stretch, not flex-start: the column has to be given the row's height for
       the scroller inside it to have something to fill. */
    align-items: stretch;
  }

  .studio-stage {
    flex: 1 1 auto;
  }

  /*
    The picture decides how tall this row is, and this column takes what it is
    given.

    It used to be the other way round. The column grows with what is selected —
    a title has a font, a size, a colour and an outline; a placed layer has a
    transform and an animation list — and being the taller of the two it set the
    height of the row, pushing the timeline down the page. On a laptop that puts
    the thing you spend the session dragging below the fold, which is the wrong
    way round: the inspector is where you go to adjust something, the timeline is
    where you work.

    So the contents come out of the flow, leaving the column with no height of
    its own to contribute, and the scroller fills whatever the picture beside it
    turned out to need.
  */
  .studio-side {
    position: relative;
    flex: 0 0 clamp(320px, 30%, 46ch);
  }

  .studio-side-scroll {
    position: absolute;
    inset: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Room for the scrollbar, so a slider's right edge is not underneath it. */
    padding-right: var(--sp-2);
  }

  .studio-screen {
    width: min(100%, calc(60vh * var(--fw, 16) / var(--fh, 9)));
  }
}

.studio-transport {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

/* The scrub bar is the only thing in this row that wants width, so everything
   else is pinned to its own size. Without this a control that is `width: 100%`
   by default — a <select>, say — takes the row and leaves the bar a dot. */
.studio-transport > :not(.studio-scrub) {
  flex: 0 0 auto;
  width: auto;
}

.studio-scrub {
  flex: 1 1 auto;
  min-width: 0;
}

/* The screen ----------------------------------------------------------- */

/* A hole in the timeline is black on screen, and saying so beats a frozen
   last frame that reads as a stuck player. */
.studio-dark {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: var(--r-md);
  background: #000;
}

/* Which of the two previews is on screen. One is the footage itself at full
   resolution, which is the sharpest picture available and cannot show a
   dissolve; the other is the timeline as it will render. Worth stating, because
   the difference — in sharpness, and in what is composited — is exactly the
   kind a person would otherwise read as a bug in their edit. */
.studio-honesty {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  padding: 2px 8px;
  border: 1px solid rgb(255 255 255 / 18%);
  border-radius: 999px;
  background: rgb(0 0 0 / 55%);
  color: rgb(255 255 255 / 78%);
  font-size: 11px;
  letter-spacing: 0.02em;
  pointer-events: none;
  backdrop-filter: blur(4px);
}

.studio-honesty.exact {
  border-color: color-mix(in srgb, var(--ok, #3ba55d) 55%, transparent);
  color: color-mix(in srgb, var(--ok, #3ba55d) 85%, white);
}

.studio-honesty.building {
  border-color: color-mix(in srgb, var(--accent, #7aa2f7) 45%, transparent);
}

/* The sources, unencoded. Marked in the same green as the rendered layer,
   because both are honest about this moment — the distinction that matters is
   "trust this" against "approximate", not which of the two it is. */
.studio-honesty.sharp {
  border-color: color-mix(in srgb, var(--ok, #3ba55d) 40%, transparent);
  color: color-mix(in srgb, var(--ok, #3ba55d) 70%, white);
}

/* Wide enough that "Sharp" and "Fast" occupy the same space, so the row does
   not shift when it is clicked. */
.studio-sharp {
  min-width: 62px;
}

/* The timeline --------------------------------------------------------- */

.tl {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--s2);
  overflow: hidden;
}

.tl-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 4px 8px;
  border-bottom: 1px solid var(--line);
  background: var(--s3);
}

.tl-scroll {
  position: relative;
  overflow: auto hidden;
  /* Room for three lanes and the ruler before it starts scrolling vertically;
     beyond that the page scrolls, which is better than a second scrollbar. */
  max-height: 340px;
  overscroll-behavior-x: contain;
}

.tl-inner {
  position: relative;
  min-width: 100%;
}

/* The ruler ------------------------------------------------------------ */

.tl-ruler {
  position: relative;
  height: 22px;
  border-bottom: 1px solid var(--line);
  background: var(--s3);
  cursor: text;
  user-select: none;
}

.tl-tick {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--line);
}

.tl-tick.half {
  top: 12px;
  opacity: 0.6;
}

.tl-tick span {
  position: absolute;
  top: 2px;
  left: 4px;
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  color: var(--fg-3);
  white-space: nowrap;
}

/* Lanes ---------------------------------------------------------------- */

.tl-lanes {
  position: relative;
}

.tl-lane {
  position: relative;
  border-bottom: 1px solid var(--line);
  background: repeating-linear-gradient(90deg,
    transparent 0 39px, rgb(255 255 255 / 3%) 39px 40px);
}

.tl-lane.base {
  background-color: rgb(255 255 255 / 2%);
}

.tl-lane-name {
  position: sticky;
  left: 0;
  z-index: 2;
  display: inline-block;
  padding: 1px 6px;
  font-size: 10px;
  color: var(--fg-3);
  background: var(--s2);
  border-radius: 0 0 var(--r-sm) 0;
  pointer-events: none;
}

/* Pieces --------------------------------------------------------------- */

.tl-piece {
  position: absolute;
  top: 2px;
  bottom: 2px;
  overflow: hidden;
  background: var(--s3);
  border: 1px solid var(--border-2);
  border-radius: var(--r-sm);
  cursor: grab;
  touch-action: none;
}

.tl-piece.on {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.tl-piece.muted .tl-wave {
  display: none;
}

/*
  A piece with a job running on it.

  The point of doing this on the piece rather than only in a panel is that the
  question "which shot is being replaced" has a place on the screen where the
  answer already is. A sheen across it and a bar along its bottom: enough to
  read at a glance across a wide timeline, not so much that the footage
  underneath stops being legible — you still have to be able to see the shot to
  decide what to do with the rest of the cut while it renders.
*/
.tl-piece.working {
  border-color: var(--accent-line);
}

.tl-work {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
}

.tl-piece.working .tl-work {
  opacity: 1;
  background: linear-gradient(100deg,
    transparent 20%, var(--accent-soft) 45%, rgb(255 255 255 / 14%) 50%,
    var(--accent-soft) 55%, transparent 80%);
  background-size: 260% 100%;
  animation: tl-sheen 1.9s linear infinite;
}

/* Queued, not started: still marked, but not pretending to be busy. */
.tl-piece.waiting .tl-work {
  animation-duration: 4.5s;
  opacity: 0.55;
}

@keyframes tl-sheen {
  from { background-position: 130% 0; }
  to { background-position: -130% 0; }
}

.tl-work > i {
  position: absolute;
  inset: auto 0 0 0;
  display: block;
  width: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  transition: width var(--t-med);
}

/* Nothing to count yet, so it sweeps rather than sitting at zero. */
.tl-piece.vague .tl-work > i {
  width: 30%;
  animation: slide 1.4s ease-in-out infinite;
}

.tl-strip {
  position: absolute;
  inset: 0 0 20px 0;
  overflow: hidden;
}

.tl-strip img {
  position: absolute;
  top: 0;
  bottom: 0;
  height: 100%;
  object-fit: cover;
  opacity: 0.8;
  pointer-events: none;
}

.tl-wave {
  position: absolute;
  inset: auto 0 14px 0;
  width: 100%;
  height: 20px;
  pointer-events: none;
}

.tl-name {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  gap: 6px;
  padding: 1px 5px;
  font-size: 10px;
  color: #fff;
  background: linear-gradient(transparent, rgb(0 0 0 / 85%));
  pointer-events: none;
}

.tl-name .truncate {
  flex: 1;
  min-width: 0;
}

/*
  The slack: how much of the source is left past each edge. Every editor makes
  you find this by dragging until you hit a wall; drawn, it is a place on the
  screen instead.
*/
.tl-slack {
  position: absolute;
  top: 0;
  bottom: 0;
  background: repeating-linear-gradient(135deg,
    rgb(255 255 255 / 10%) 0 4px, transparent 4px 8px);
  pointer-events: none;
}

.tl-slack.left { left: 0; }
.tl-slack.right { right: 0; }

/* Where two pieces are on screen at once. */
.tl-xf {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  background: linear-gradient(90deg, var(--accent-soft, rgb(99 102 241 / 45%)), transparent);
  border-left: 1px solid var(--accent);
  pointer-events: none;
}

/* An animation stop, drawn as the diamond everyone already reads as one. */
.tl-stop {
  position: absolute;
  top: 3px;
  width: 7px;
  height: 7px;
  margin-left: -3.5px;
  background: #fff;
  border: 1px solid rgb(0 0 0 / 55%);
  transform: rotate(45deg);
  pointer-events: none;
}

/* Past the end of the footage: an offer, so it is dashed rather than solid. */
.tl-past {
  position: absolute;
  top: 0;
  bottom: 0;
  display: grid;
  place-items: center;
  font-size: 10px;
  color: var(--warn);
  border: 1px dashed var(--warn);
  background: rgb(251 191 36 / 12%);
  pointer-events: none;
}

.tl-past.left { right: 100%; }
.tl-past.right { left: 100%; }

.tl-grip {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 9px;
  cursor: ew-resize;
  background: linear-gradient(var(--accent), var(--accent));
  opacity: 0;
  transition: opacity var(--t-fast);
}

.tl-grip.left { left: 0; }
.tl-grip.right { right: 0; }

.tl-piece:hover .tl-grip,
.tl-piece.on .tl-grip {
  opacity: 0.65;
}

/*
  A hole in the base lane: the thing a generated bridge fills.

  It stayed inert for several slices, which made it a caption rather than a
  control. Now it is the one place on the timeline where the obvious move is to
  generate something, so it holds the button that does it — visible on hover,
  because a row of permanent buttons down a cut-up timeline is noise.
*/
.tl-hole {
  position: absolute;
  top: 8px;
  bottom: 8px;
  overflow: hidden;
  font-size: 10px;
  color: var(--fg-3);
  border: 1px dashed var(--line);
  border-radius: var(--r-sm);
}

.tl-hole-said {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.tl-hole-fill {
  position: absolute;
  inset: 0;
  display: flex;
  gap: 4px;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--accent-2);
  font-size: 11px;
  background: var(--accent-soft);
  border: 0;
  border-radius: inherit;
  opacity: 0;
  cursor: pointer;
  transition: opacity var(--t-fast);
}

.tl-hole-fill .icon {
  width: 13px;
  height: 13px;
}

.tl-hole:hover .tl-hole-fill,
.tl-hole-fill:focus-visible {
  opacity: 1;
}

/* A bridge on its way, marked the way a piece being worked on is. */
.tl-hole.working {
  color: var(--accent-2);
  border-style: solid;
  border-color: var(--accent-line);
  background: linear-gradient(100deg,
    transparent 20%, var(--accent-soft) 45%, rgb(255 255 255 / 14%) 50%,
    var(--accent-soft) 55%, transparent 80%);
  background-size: 260% 100%;
  animation: tl-sheen 1.9s linear infinite;
}

.tl-hole.waiting {
  animation-duration: 4.5s;
  opacity: 0.7;
}

/* Nothing to fill while one is running: the hole is spoken for. */
.tl-hole.working .tl-hole-fill {
  display: none;
}

.tl-hole-work {
  position: absolute;
  inset: auto 0 0 0;
  display: none;
  width: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  transition: width var(--t-med);
}

.tl-hole.working .tl-hole-work {
  display: block;
}

.tl-hole.vague .tl-hole-work {
  width: 30%;
  animation: slide 1.4s ease-in-out infinite;
}

/* The playhead and its friends ----------------------------------------- */

.tl-playhead {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: var(--danger, #f43f5e);
  pointer-events: none;
  z-index: 3;
}

.tl-playhead::before {
  content: "";
  position: absolute;
  top: 0;
  left: -5px;
  border: 6px solid transparent;
  border-top-color: var(--danger, #f43f5e);
}

/* Where the razor would land. Dashed, and thinner than the playhead, so a
   line that is about to cut cannot be mistaken for the line you are watching —
   solid when it has caught a snap point, which is the confirmation that a cut
   aimed at the playhead will land exactly on it. */
.tl-blade {
  position: absolute;
  top: 22px;
  bottom: 0;
  width: 0;
  border-left: 1px dashed var(--warn, #f59e0b);
  pointer-events: none;
  z-index: 4;
}

.tl-blade.snapped {
  border-left-style: solid;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--warn, #f59e0b) 35%, transparent);
}

/* With the razor out, everything in the lanes is a place to cut rather than a
   thing to pick up, and the cursor is the only warning of that before a click. */
.tl.razor .tl-lanes,
.tl.razor .tl-piece,
.tl.razor .tl-grip {
  cursor: crosshair;
}

.tl-tool.on {
  background: var(--accent-soft);
  color: var(--accent);
}

.tl-sep {
  width: 1px;
  align-self: stretch;
  margin: 2px 2px;
  background: var(--line);
}

/* Where a drag is about to land, while it is still deciding. */
.tl-guide {
  position: absolute;
  top: 22px;
  bottom: 0;
  width: 1px;
  background: var(--accent);
  pointer-events: none;
  z-index: 3;
}

.tl-band {
  position: absolute;
  background: rgb(99 102 241 / 18%);
  border: 1px solid var(--accent);
  pointer-events: none;
}

/* Below the strip ------------------------------------------------------ */

.studio-inspector,
.studio-bed,
.studio-finish,
.studio-render {
  padding: var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--s2);
}

/* The three rows in here — the size, the sentence about it, the adult switch —
   are separate questions and were stacked flush against each other, the last one
   reading as a third line of the sentence above it. */
.studio-finish {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/* Three numbers that have to stay on one line with room for four digits, next
   to a sentence that is allowed to wrap under them. */
.studio-finish input[type="number"] {
  width: 68px;
  text-align: center;
}

.studio-finish .row + .row {
  margin-top: var(--sp-2);
}

.studio-render:empty {
  display: none;
}

.studio-bed input[type="range"] {
  width: 110px;
}

.studio-controls {
  gap: var(--sp-3);
  margin-top: var(--sp-2);
}

.time-field {
  width: 84px;
  text-align: center;
}

/* Takes ---------------------------------------------------------------- */

/*
  Rows rather than a strip of thumbnails: what tells two takes of the same shot
  apart is which job made it and how long it is, not a 60px picture of a frame
  that is nearly the same picture in all of them.
*/
.studio-takes {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}

.studio-take {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  min-width: 0;
  padding: 4px;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
}

.studio-take.on {
  background: var(--s3);
  border-color: var(--line);
}

.studio-take img {
  flex: 0 0 auto;
  width: 56px;
  height: 34px;
  object-fit: cover;
  border-radius: var(--r-sm);
  background: var(--s3);
}

.studio-title {
  min-width: 0;
  max-width: 340px;
  padding: 6px 10px;
  font-size: 18px;
  font-weight: 600;
  color: var(--fg-1);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
}

.studio-title:hover {
  border-color: var(--line);
}

.studio-title:focus {
  border-color: var(--accent);
  background: var(--s2);
  outline: none;
}

/* Working on one shot -------------------------------------------------- */

/*
  Wider than a confirm box because it holds a form: a job asks for a clip, a
  character and a line of text, and those are full-width controls. Stated after
  app.css sets the 440px cap, which is the only way a variant beats it.
*/
.shot-card {
  gap: var(--sp-3);
  max-width: min(680px, 96vw);
}

.shot-job-thumb {
  width: 64px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--r-sm);
  background: var(--s3);
}

.shot-job-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/*
  The sharpening sheet: a row of sizes to aim at, then what that would cost.

  The sizes read as one control rather than four buttons because they are one
  choice, and the resulting frame sits on the same line as the thing that
  decides it — the whole sheet is "pick a size, read the bill".
*/
.sharp-targets {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--line);
}

.sharp-targets .muted {
  margin-left: auto;
}

.sharp-sum {
  padding-block: var(--sp-3);
}

.sharp-sum p {
  margin: 0;
}

.sharp-sum p + p {
  margin-top: var(--sp-1);
}

/*
  The two ends of a hole, and the distance between them.

  Shown rather than named because a bridge is judged on how it leaves and how
  it arrives, so the two frames it has to leave from and arrive at are the whole
  brief — and seeing them is the only way to tell before spending the GPU that
  the right hole was clicked.
*/
.gap-ends {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  padding-block: var(--sp-2);
  border-bottom: 1px solid var(--line);
}

.gap-end {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.gap-end img {
  width: 100%;
  max-height: 130px;
  object-fit: contain;
  border-radius: var(--r-sm);
  background: #000;
}

/* ------------------------------------------------- erasing part of a layer */

.cut-card {
  width: min(920px, 96vw);
}

.cut-stage {
  display: grid;
  place-items: center;
  padding: var(--sp-3);
  background: var(--bg);
  border-radius: var(--r-md);
}

/* The checkerboard *is* the feature: erasing is only judgeable against what
   will show through, and a red overlay cannot answer "does it read yet". */
.cut-frame {
  position: relative;
  width: 100%;
  max-height: 58vh;
  line-height: 0;
  background-color: #2a2a2a;
  background-image:
    linear-gradient(45deg, #3a3a3a 25%, transparent 25%, transparent 75%, #3a3a3a 75%),
    linear-gradient(45deg, #3a3a3a 25%, transparent 25%, transparent 75%, #3a3a3a 75%);
  background-position: 0 0, 11px 11px;
  background-size: 22px 22px;
  border-radius: var(--r-sm);
}

.cut-canvas {
  display: block;
  width: 100%;
  height: 100%;
  max-height: 58vh;
  object-fit: contain;
  cursor: crosshair;
  touch-action: none;
}

.cut-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  padding-top: var(--sp-2);
}

.cut-tools .field {
  width: 170px;
}

/* What is stacked over the base lane at this end, which the thumbnail beside it
   cannot show: the thumbnail is one piece's frame, the grab is the composite. */
.gap-over {
  color: var(--accent-2);
}

/* The choice between the picture as composed and one piece's own footage. Laid
   out as a label so the whole two-line block is the hit target. */
.gap-compose {
  display: flex;
  gap: var(--sp-2);
  align-items: flex-start;
  padding-block: var(--sp-2);
  border-bottom: 1px solid var(--line);
  cursor: pointer;
}

.gap-compose input {
  margin-top: 3px;
  flex: none;
}

.gap-arrow {
  display: flex;
  flex: none;
  flex-direction: column;
  gap: 4px;
  align-items: center;
  color: var(--fg-3);
  font-size: 11px;
}

.gap-line {
  width: 42px;
  height: 0;
  border-top: 1px dashed var(--fg-3);
}

/* No frame to arrive at: the shot runs on rather than landing anywhere. */
.gap-open {
  display: grid;
  place-items: center;
  height: 130px;
  padding: var(--sp-2);
  color: var(--fg-3);
  font-size: 11px;
  text-align: center;
  border: 1px dashed var(--line);
  border-radius: var(--r-sm);
}

.shot-job-list {
  display: grid;
  gap: var(--sp-2);
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}

.shot-job-list .tile.off {
  opacity: 0.55;
}

/*
  The settings fold.

  Shut, it has to read as one line among the questions rather than as another
  question. Open, it is the create page's own controls, so it only needs a lane
  to stack them in and a rule to sit under.
*/
.job-advanced {
  border-top: 1px solid var(--line);
  padding-top: var(--sp-3);
}

.job-advanced > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  cursor: pointer;
  color: var(--fg-2);
  font-size: 0.9rem;
  list-style: none;
}

.job-advanced > summary::-webkit-details-marker {
  display: none;
}

.job-advanced > summary:hover {
  color: var(--fg);
}

.job-advanced > summary .icon {
  flex: none;
}

/* The workflow's name, which is long and is the least of the three things. */
.job-advanced > summary > span:last-child {
  min-width: 0;
  margin-left: auto;
  text-align: right;
}

.job-knobs {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding-top: var(--sp-3);
}

/* What is being generated ---------------------------------------------- */

/*
  Parked in the corner rather than in the layout, because it is only there some
  of the time and a column that appears and disappears would shove the timeline
  sideways every time a job started. Narrow on purpose: it says what is running
  and how far, and anything more belongs on the piece it is about.
*/
.studio-dock {
  position: fixed;
  right: var(--sp-5);
  bottom: var(--sp-5);
  z-index: 88;
  width: 290px;
  max-width: calc(100vw - var(--sp-5) * 2);
  background: color-mix(in srgb, var(--s2) 92%, transparent);
  border: 1px solid var(--border-2);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-3);
  backdrop-filter: blur(6px);
}

/*
  Out of the way while anything is open over the page, for the reason the
  assistant bubble is — see css/studio-chat.css, which explains it properly. Same
  corner, same trick, and the same 88 that puts it above the dialogs at 85: on a
  phone this is the width of the screen, so a dialog opening underneath it left
  its top corner unreachable.
*/
body.overlaid .studio-dock,
body:has(.modal) .studio-dock {
  visibility: hidden;
}

.studio-dock-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 6px 6px 6px var(--sp-3);
  font-weight: 600;
}

.studio-dock-fold span {
  font-size: 11px;
  line-height: 1;
}

.studio-dock-rows {
  display: flex;
  flex-direction: column;
  max-height: 44vh;
  overflow: auto;
  border-top: 1px solid var(--border);
}

.studio-dock-row {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  padding: var(--sp-2) 6px var(--sp-2) var(--sp-3);
}

.studio-dock-row + .studio-dock-row {
  border-top: 1px solid var(--border);
}

.studio-dock-row.bad {
  border-left: 2px solid var(--danger);
}

.studio-dock-row .bar.tiny-bar {
  height: 3px;
  margin-top: 5px;
}

/* Which shot it is for, as a link rather than a label: the shot may be off the
   left of a zoomed-in timeline, and the point of naming it is to get to it. */
.studio-dock-where {
  flex: none;
  max-width: 45%;
  padding: 0 4px;
  overflow: hidden;
  color: var(--accent-2);
  font-size: 11px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.badge.working {
  color: var(--accent-2);
  background: var(--accent-soft);
}

@media (width <= 720px) {
  .studio-preview {
    max-height: 38vh;
  }

  .studio-title {
    max-width: 100%;
  }

  .studio-dock {
    right: var(--sp-3);
    width: calc(100vw - var(--sp-3) * 2);
  }
}

/*
  Above the tab bar, once the nav is a bar across the bottom instead of a column
  down the left.

  It is pinned to the viewport, not to the page, so at the width where the nav
  moves under it the dock covered it — full width, and covering the way out of
  the page rather than a corner of it. The assistant stacks on top of this and
  already clears the bar by the same amount; until now the two disagreed about
  where the bottom was, by exactly the height of the bar.

  Kept in step with css/studio-chat.css, which has the reasoning and the numbers:
  the bar's own 52px minimum plus whatever the phone reserves beneath it. After
  the width rules above, so it wins where the two overlap.
*/
@media (max-width: 860px) {
  .studio-dock {
    bottom: calc(var(--sp-3) + 52px + env(safe-area-inset-bottom));
  }
}
