Dropping footage on the stage or the timeline asks which frames and what name: a dialog plays the video with start and end handles that seek it. Detection runs on that range only (decoding stops at the end, frames before the start are skipped) and the range is part of the analysis address, so a partial analysis is never served as a whole one. The frozen take comes in as one named symbol, carrying its sound as an audio node, placed where it was dropped; tracking and tuning come with it when the document has no analysis of its own. clip/adopt copies symbols between documents, renaming ids that collide, and clip/audio-tracks carries sounds out of nested instances so a placed symbol is heard where it is placed. Fixes the stage going blank after a conversion: ::store recomputed only when the clip id changed, so blocks merged into the loaded entry were invisible to the resolver. And the paint loop now schedules its next frame before painting and reports a frame it cannot draw instead of stopping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
727 lines
18 KiB
CSS
727 lines
18 KiB
CSS
/* The application chrome: one screen, five panes, no scrolling page.
|
|
*
|
|
* Macromedia-era, deliberately. Light grey panels, hairline rules, small type,
|
|
* and a timeline whose keyframes are DOTS on a frame grid — Flash's vocabulary,
|
|
* because this tool's model is Flash's model (a library of timelines, instances
|
|
* placed on a stage, a layer list against a ruler) and borrowing the look is the
|
|
* cheapest way to make that legible to anyone who has used one. What is not
|
|
* borrowed is the chrome of the period: no bevels, no gradients on buttons, no
|
|
* inset wells. Flat, clean, one accent.
|
|
*
|
|
* Raw CSS on purpose. This file styles a fixed grid of panes whose class names
|
|
* are the domain's own — `.tl-row`, `.pool-item`, `.swatch` — so a utility
|
|
* framework would buy naming we want anyway, at the cost of a third process in
|
|
* a dev loop the repo keeps at two. Tokens are custom properties; everything
|
|
* else is a handful of grids.
|
|
*
|
|
* It lives under static/ rather than inline in the template because it is now
|
|
* long enough that "the styles" and "the page" are two things. */
|
|
|
|
:root {
|
|
color-scheme: light;
|
|
|
|
/* The stage's background is the PALETTE's index 0, not a theme colour. The
|
|
two agreeing is what makes the preview honest, so it is written here as the
|
|
same value arthur.domain.palette declares and nowhere else in this file.
|
|
It is the one dark surface on the page, and it should be: the picture is
|
|
320x200 of indexed colour and the UI is not allowed to compete with it. */
|
|
--stage: #12141c;
|
|
|
|
--desk: #8e8e8e; /* the work area the stage floats on */
|
|
--pane: #f2f2f0;
|
|
--chrome: #e4e4e1;
|
|
--sunk: #ebebe8;
|
|
--line: #b0b0ac;
|
|
--hair: #d2d2ce;
|
|
--fg: #1f1f1f;
|
|
--dim: #6e6e6a;
|
|
|
|
/* One accent, used for selection and for nothing else. */
|
|
--sel: #2f6fc0;
|
|
--sel-bg: #cfe0f5;
|
|
|
|
/* A keyframe is a dot and a dot is ink. Flash draws them black and so does
|
|
this; the frames a node exists over are a pale tint behind them. */
|
|
--key: #1f1f1f;
|
|
--span: #dfe8f4;
|
|
--span-line:#a9bdd8;
|
|
--grid: #dcdcd8;
|
|
--grid-5: #c6c6c2;
|
|
--playhead: #c8322b;
|
|
--warn: #b4502a;
|
|
|
|
--row: 21px;
|
|
--ruler: 19px;
|
|
--label: 210px;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
html, body { margin: 0; height: 100%; overflow: hidden; }
|
|
|
|
body {
|
|
background: var(--desk);
|
|
color: var(--fg);
|
|
font: 11px/1.45 "Lucida Grande", "Segoe UI", system-ui, sans-serif;
|
|
}
|
|
|
|
#app { height: 100%; }
|
|
|
|
/* The preview is nearest-neighbour everywhere. A browser that smoothed the
|
|
upscale would misrepresent the exact look the tool exists to judge, so this
|
|
rule is load-bearing rather than cosmetic. */
|
|
canvas { image-rendering: pixelated; }
|
|
|
|
audio { display: none; }
|
|
|
|
/* --------------------------------------------------------------------------
|
|
the frame */
|
|
|
|
.app {
|
|
display: grid;
|
|
height: 100%;
|
|
grid-template-columns: var(--label) minmax(0, 1fr) 250px;
|
|
grid-template-rows: 30px minmax(0, 1fr) 232px;
|
|
grid-template-areas:
|
|
"top top top"
|
|
"pool view params"
|
|
"time time time";
|
|
gap: 1px;
|
|
background: var(--line);
|
|
}
|
|
|
|
.top { grid-area: top; }
|
|
.pool { grid-area: pool; }
|
|
.view { grid-area: view; }
|
|
.params { grid-area: params; }
|
|
.time { grid-area: time; }
|
|
|
|
/* Every pane is its own scroll container. `min-height: 0` is what lets a grid
|
|
row shrink below its content instead of pushing the layout taller than the
|
|
viewport — the single line that separates "a page that scrolls" from "an
|
|
application window". */
|
|
.pane {
|
|
background: var(--pane);
|
|
min-height: 0;
|
|
min-width: 0;
|
|
overflow: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.pane-head {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 2;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 0 7px;
|
|
height: 21px;
|
|
flex: 0 0 21px;
|
|
background: var(--chrome);
|
|
border-bottom: 1px solid var(--line);
|
|
color: var(--dim);
|
|
letter-spacing: .03em;
|
|
}
|
|
|
|
.pane-head .spacer { flex: 1; }
|
|
.pane-body { padding: 5px; }
|
|
|
|
/* --------------------------------------------------------------------------
|
|
controls */
|
|
|
|
button {
|
|
font: inherit;
|
|
color: var(--fg);
|
|
background: var(--pane);
|
|
border: 1px solid var(--line);
|
|
border-radius: 2px;
|
|
padding: 1px 7px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
button:hover:not(:disabled) { background: #fff; }
|
|
button:active:not(:disabled) { background: var(--sunk); }
|
|
button:disabled { opacity: .38; cursor: default; }
|
|
|
|
button.on {
|
|
background: var(--sel-bg);
|
|
border-color: var(--sel);
|
|
color: #14395f;
|
|
}
|
|
|
|
select, input[type="text"] {
|
|
font: inherit;
|
|
color: var(--fg);
|
|
background: #fff;
|
|
border: 1px solid var(--line);
|
|
border-radius: 2px;
|
|
padding: 1px 3px;
|
|
max-width: 100%;
|
|
}
|
|
|
|
input[type="range"] { width: 100%; accent-color: var(--sel); }
|
|
|
|
.row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.dim { color: var(--dim); }
|
|
.warn { color: var(--warn); }
|
|
|
|
/* --------------------------------------------------------------------------
|
|
top bar */
|
|
|
|
.top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
padding: 0 8px;
|
|
background: var(--chrome);
|
|
}
|
|
|
|
.top .brand { color: var(--dim); letter-spacing: .14em; text-transform: uppercase; }
|
|
.top .status {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--dim);
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
the open menu */
|
|
|
|
.menu-wrap { position: relative; }
|
|
|
|
/* A full-page catcher behind the panel, so a click anywhere else dismisses it.
|
|
More predictable than a document listener that has to be added, removed, and
|
|
told to ignore the click that opened the menu. */
|
|
.menu-scrim { position: fixed; inset: 0; z-index: 40; }
|
|
|
|
.menu {
|
|
position: absolute;
|
|
top: calc(100% + 3px);
|
|
right: 0;
|
|
z-index: 41;
|
|
min-width: 250px;
|
|
max-height: 72vh;
|
|
overflow: auto;
|
|
padding: 5px;
|
|
background: var(--pane);
|
|
border: 1px solid var(--line);
|
|
border-radius: 2px;
|
|
box-shadow: 0 3px 10px rgba(0, 0, 0, .3);
|
|
}
|
|
|
|
.menu > h2 {
|
|
margin: 6px 0 3px;
|
|
font: inherit;
|
|
font-weight: 600;
|
|
color: var(--dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: .07em;
|
|
}
|
|
|
|
.menu > h2:first-child { margin-top: 0; }
|
|
.menu > h2 + .dim { margin-bottom: 4px; }
|
|
|
|
.menu-item {
|
|
display: block;
|
|
width: 100%;
|
|
text-align: left;
|
|
padding: 2px 6px;
|
|
border: 1px solid transparent;
|
|
border-radius: 2px;
|
|
background: none;
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.menu-item:hover:not(:disabled) { background: var(--sel-bg); }
|
|
|
|
/* A fixture is not a document. Italic is the whole of the distinction, and it is
|
|
enough: the heading above already says what these are. */
|
|
.menu-item.example { font-style: italic; }
|
|
.menu-item .sub { display: block; color: var(--dim); }
|
|
|
|
/* --------------------------------------------------------------------------
|
|
media pool */
|
|
|
|
/* Two folders, THIS PROJECT and ALL ASSETS, each holding groups. */
|
|
.pool-folder { margin-bottom: 8px; }
|
|
.pool-folder > summary {
|
|
cursor: pointer;
|
|
padding: 2px 0 4px;
|
|
font-weight: 600;
|
|
color: var(--fg);
|
|
}
|
|
.pool-folder > .pool-group { padding-left: 8px; }
|
|
.pool-project { margin-bottom: 2px; }
|
|
.pool-project > summary { cursor: pointer; color: var(--dim); padding: 1px 0; }
|
|
.pool-project > .pool-item { margin-left: 10px; width: calc(100% - 10px); }
|
|
|
|
.pool-group { margin-bottom: 9px; }
|
|
|
|
/* The one-line gloss under a group heading. Small and quiet: it answers "what
|
|
is this list" once, for someone who has not read the model. */
|
|
.pool-group > h2 + .dim { margin-bottom: 4px; }
|
|
|
|
.pool-group > h2 {
|
|
margin: 0 0 3px;
|
|
font: inherit;
|
|
font-weight: 600;
|
|
color: var(--dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: .07em;
|
|
}
|
|
|
|
.pool-item {
|
|
display: block;
|
|
width: 100%;
|
|
text-align: left;
|
|
padding: 2px 6px;
|
|
border: 1px solid transparent;
|
|
border-radius: 2px;
|
|
background: none;
|
|
cursor: grab;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.pool-item:hover:not(:disabled) { background: #fff; }
|
|
.pool-item.on { background: var(--sel-bg); border-color: var(--sel); }
|
|
.pool-item .sub { display: block; color: var(--dim); }
|
|
.pool-item .text { display: block; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
/* A media row leads with one frame of the video. */
|
|
.pool-item.media { display: flex; align-items: center; gap: 6px; }
|
|
.pool-item .thumb {
|
|
flex: 0 0 40px;
|
|
width: 40px;
|
|
height: 30px;
|
|
max-width: 40px;
|
|
max-height: 30px;
|
|
object-fit: cover;
|
|
background: var(--stage);
|
|
border-radius: 2px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* The whole pane is the drop target, so the cue has to be the pane and not a
|
|
zone inside it — a drop that lands two pixels outside a dashed rectangle and
|
|
silently does nothing is the failure this avoids. */
|
|
.pool.dropping {
|
|
outline: 2px dashed var(--sel);
|
|
outline-offset: -3px;
|
|
background: var(--sel-bg);
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
the viewer: palette above, stage below */
|
|
|
|
.view {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--desk);
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* One tab per open symbol: flat, on the chrome, with the open one marked by the
|
|
selection colour underneath rather than by a raised shape. */
|
|
.tabs {
|
|
display: flex;
|
|
align-items: stretch;
|
|
height: 24px;
|
|
flex: 0 0 24px;
|
|
padding: 0 4px;
|
|
background: var(--chrome);
|
|
border-bottom: 1px solid var(--line);
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.tab {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 0 6px 0 10px;
|
|
color: var(--dim);
|
|
border-right: 1px solid var(--hair);
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.tab:hover { color: var(--fg); background: var(--sunk); }
|
|
.tab.on { color: var(--fg); background: var(--pane); box-shadow: inset 0 -2px 0 var(--sel); }
|
|
|
|
.tab .close {
|
|
width: 16px;
|
|
height: 16px;
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 2px;
|
|
background: none;
|
|
color: var(--dim);
|
|
line-height: 16px;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.tab:hover .close, .tab.on .close { visibility: visible; }
|
|
.tab .close:hover { background: var(--hair); color: var(--fg); }
|
|
|
|
.palette-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 9px;
|
|
padding: 4px 8px;
|
|
background: var(--chrome);
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.swatches { display: flex; gap: 3px; }
|
|
|
|
/* Circles. A palette entry is one indivisible tone, not an area of coverage, and
|
|
a row of dots says that where a row of tiles says "swatch book". */
|
|
.swatch {
|
|
width: 15px;
|
|
height: 15px;
|
|
padding: 0;
|
|
border: 1px solid var(--line);
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.swatch:hover:not(:disabled) { border-color: var(--fg); }
|
|
|
|
/* A swatch that cannot be picked still has to SHOW ITS COLOUR: the generic
|
|
disabled rule fades it towards the pane, which turned the background slot —
|
|
the darkest tone in the palette — into pale grey, i.e. into a lie about what
|
|
index 0 is. Slot 0 says what it is with a ring instead. */
|
|
.swatch:disabled { opacity: 1; }
|
|
.swatch.bg { cursor: default; box-shadow: inset 0 0 0 2px var(--pane); }
|
|
|
|
.swatch.on {
|
|
border-color: var(--fg);
|
|
box-shadow: 0 0 0 2px var(--pane), 0 0 0 3px var(--sel);
|
|
}
|
|
|
|
/* An empty slot has to read as "no colour here" rather than as a pale colour,
|
|
which a flat fill at this size cannot do. */
|
|
.swatch.empty {
|
|
cursor: default;
|
|
background: #fff;
|
|
border-style: dashed;
|
|
border-color: var(--hair);
|
|
}
|
|
|
|
.stage-area {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: grid;
|
|
place-items: center;
|
|
overflow: auto;
|
|
padding: 14px;
|
|
}
|
|
|
|
/* The stage sits ON the work area rather than filling it, as it does in every
|
|
tool of this shape: the picture has edges and they are part of what you are
|
|
judging. */
|
|
.stage-wrap {
|
|
position: relative;
|
|
line-height: 0;
|
|
box-shadow: 0 1px 5px rgba(0, 0, 0, .35);
|
|
outline: 1px solid rgba(0, 0, 0, .45);
|
|
}
|
|
|
|
.stage { display: block; background: var(--stage); }
|
|
|
|
.paint-overlay { position: absolute; inset: 0; touch-action: none; }
|
|
.paint-overlay.drawing { cursor: crosshair; }
|
|
.paint-overlay circle { cursor: grab; }
|
|
|
|
/* Where a drag out of the pool would land. Dashed and unfilled, so it reads as
|
|
"not there yet" over whatever is already drawn. */
|
|
/* Never a hit target: it is drawn under the pointer, and anything under the
|
|
pointer that changes mid-drag gets a dragenter of its own. */
|
|
.paint-overlay .ghost { pointer-events: none; }
|
|
.paint-overlay .ghost > * {
|
|
fill: none;
|
|
stroke: #fff1be;
|
|
stroke-width: 1;
|
|
stroke-dasharray: 3 2;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
params */
|
|
|
|
.section { border-bottom: 1px solid var(--hair); padding: 6px; }
|
|
.section:last-child { border-bottom: 0; }
|
|
|
|
.section > h2 {
|
|
margin: 0 0 5px;
|
|
font: inherit;
|
|
font-weight: 600;
|
|
color: var(--dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: .07em;
|
|
}
|
|
|
|
/* Label / value, once, for every read-only fact in the pane. */
|
|
.facts { display: grid; grid-template-columns: auto 1fr; gap: 2px 8px; margin: 0; }
|
|
.facts dt { color: var(--dim); }
|
|
.facts dd { margin: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
.knob { display: grid; gap: 1px; margin-bottom: 6px; }
|
|
.knob .top-line { display: flex; justify-content: space-between; gap: 8px; }
|
|
.knob .top-line .name { color: var(--dim); }
|
|
|
|
.regeneration-debug {
|
|
padding: 4px 5px;
|
|
background: var(--sunk);
|
|
border: 1px solid var(--hair);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
timeline */
|
|
|
|
.time { overflow: hidden; }
|
|
|
|
.tl-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
overflow: auto;
|
|
}
|
|
|
|
.tl-labels {
|
|
flex: 0 0 var(--label);
|
|
/* A flex item's `min-width` defaults to `auto`, which floors its used size at
|
|
its MIN-CONTENT width — so a row labelled with a uuid widens the whole
|
|
column and pushes the tracks out of alignment with the ruler above them.
|
|
`0` is what makes the declared basis the actual width. */
|
|
min-width: 0;
|
|
position: sticky;
|
|
left: 0;
|
|
z-index: 1;
|
|
background: var(--pane);
|
|
border-right: 1px solid var(--line);
|
|
}
|
|
|
|
.tl-tracks {
|
|
flex: 1;
|
|
min-width: 340px;
|
|
position: relative;
|
|
background: #fff;
|
|
/* The frame grid, five frames to a division, as a background rather than as
|
|
an element per frame: a 900-frame take is 900 divs nobody needs in the DOM.
|
|
`--tick` is five frames as a percentage of the span, set from the component
|
|
because only it knows how long the clip is. */
|
|
background-image:
|
|
repeating-linear-gradient(90deg,
|
|
var(--grid-5) 0 1px, transparent 1px var(--tick, 10%));
|
|
}
|
|
|
|
.tl-label,
|
|
.tl-track {
|
|
height: var(--row);
|
|
border-bottom: 1px solid var(--hair);
|
|
}
|
|
|
|
/* The row is the containing block for its own span and keys. Without this they
|
|
resolve against `.tl-tracks` instead and every row's marks land stacked on the
|
|
first one, full height — which reads as "the timeline draws nothing" rather
|
|
than as a positioning bug. */
|
|
.tl-track { position: relative; }
|
|
|
|
.tl-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
padding-right: 6px;
|
|
cursor: default;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.tl-label.on { background: var(--sel-bg); }
|
|
.tl-label:hover:not(.on) { background: #fff; }
|
|
.tl-label .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
.tl-label .kind { color: var(--dim); }
|
|
|
|
/* A fixed-width cell whether or not there is a triangle in it, so names at the
|
|
same depth line up down the column. */
|
|
.tl-twist {
|
|
flex: 0 0 13px;
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 0;
|
|
background: none;
|
|
color: var(--dim);
|
|
cursor: pointer;
|
|
text-align: center;
|
|
}
|
|
|
|
.tl-twist:hover:not(:disabled) { background: none; color: var(--fg); }
|
|
.tl-twist:disabled { opacity: 0; cursor: default; }
|
|
|
|
.tl-ruler {
|
|
position: relative;
|
|
height: var(--ruler);
|
|
border-bottom: 1px solid var(--line);
|
|
background: var(--chrome);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.tl-ruler .tick {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
border-left: 1px solid var(--line);
|
|
padding-left: 3px;
|
|
color: var(--dim);
|
|
font-size: 10px;
|
|
line-height: var(--ruler);
|
|
}
|
|
|
|
/* The span the node exists over. Drawn under the keys so a dot on the first
|
|
frame of a span is not half-hidden by its own bar. */
|
|
.tl-span {
|
|
position: absolute;
|
|
top: 3px;
|
|
bottom: 3px;
|
|
background: var(--span);
|
|
border: 1px solid var(--span-line);
|
|
border-radius: 1px;
|
|
}
|
|
|
|
.tl-span.dense {
|
|
/* Generated, one value per frame: hatched, because ticking every frame would
|
|
be a solid block that says less than the bar behind it does. */
|
|
background: repeating-linear-gradient(
|
|
-45deg, var(--span) 0 3px, #c3d4ea 3px 6px);
|
|
}
|
|
|
|
/* The preview row of a drop in flight: its own length, where it would start. */
|
|
.tl-span.ghost, .tl-label.ghost { pointer-events: none; }
|
|
.tl-span.ghost {
|
|
background: transparent;
|
|
border: 1px dashed var(--sel);
|
|
}
|
|
.tl-label.ghost { color: var(--sel); font-style: italic; }
|
|
|
|
/* Flash's keyframe: a filled dot. */
|
|
.tl-key {
|
|
position: absolute;
|
|
top: 50%;
|
|
width: 7px;
|
|
height: 7px;
|
|
margin: -3.5px 0 0 -3.5px;
|
|
background: var(--key);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.tl-playhead {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 1px;
|
|
background: var(--playhead);
|
|
pointer-events: none;
|
|
z-index: 3;
|
|
}
|
|
|
|
.tl-playhead::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 1px;
|
|
left: -4px;
|
|
width: 9px;
|
|
height: 9px;
|
|
border-radius: 50%;
|
|
background: var(--playhead);
|
|
}
|
|
|
|
.tl-empty { padding: 9px; color: var(--dim); }
|
|
|
|
/* --------------------------------------------------------------------------
|
|
the video -> symbol dialog */
|
|
|
|
.convert-scrim {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 20;
|
|
display: grid;
|
|
place-items: center;
|
|
background: rgba(0, 0, 0, .35);
|
|
}
|
|
|
|
.convert {
|
|
width: min(560px, calc(100vw - 32px));
|
|
background: var(--pane);
|
|
border: 1px solid var(--line);
|
|
border-radius: 3px;
|
|
box-shadow: 0 6px 24px rgba(0, 0, 0, .3);
|
|
}
|
|
|
|
.convert-body { display: grid; gap: 8px; padding: 10px; }
|
|
|
|
.convert-video {
|
|
width: 100%;
|
|
max-height: 46vh;
|
|
background: var(--stage);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* The range: the kept frames lit, a handle at each end. */
|
|
.convert-range {
|
|
position: relative;
|
|
height: 18px;
|
|
margin: 0 7px;
|
|
background: var(--sunk);
|
|
border: 1px solid var(--hair);
|
|
border-radius: 2px;
|
|
touch-action: none;
|
|
}
|
|
|
|
.convert-kept {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
background: var(--sel-bg);
|
|
border-left: 1px solid var(--sel);
|
|
border-right: 1px solid var(--sel);
|
|
}
|
|
|
|
.convert-handle {
|
|
position: absolute;
|
|
top: -3px;
|
|
bottom: -3px;
|
|
width: 10px;
|
|
margin-left: -5px;
|
|
background: var(--sel);
|
|
border-radius: 2px;
|
|
cursor: ew-resize;
|
|
}
|
|
|
|
.convert-name { display: flex; align-items: center; gap: 8px; }
|
|
.convert-name input { flex: 1; }
|
|
|
|
.convert-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 6px;
|
|
padding: 8px 10px;
|
|
border-top: 1px solid var(--hair);
|
|
}
|