1256 lines
40 KiB
CSS
1256 lines
40 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;
|
||
/* The location bar is its own row and takes its height from the stage, not
|
||
from the timeline: it exists to explain what the timeline is showing, so
|
||
paying for it in timeline rows would be the wrong trade. */
|
||
grid-template-rows: 30px minmax(0, 1fr) 21px 232px;
|
||
grid-template-areas:
|
||
"top top top"
|
||
"pool view params"
|
||
"loc loc loc"
|
||
"time time time";
|
||
gap: 1px;
|
||
background: var(--line);
|
||
}
|
||
|
||
.top { grid-area: top; }
|
||
.pool { grid-area: pool; }
|
||
.view { grid-area: view; }
|
||
.params { grid-area: params; }
|
||
.loc { grid-area: loc; }
|
||
.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;
|
||
/* One line, always. A strip of controls that reflows to two lines resizes the
|
||
pane under it, and the pane under this one is the timeline. */
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pane-head .readout { color: var(--fg); }
|
||
|
||
/* A pick-one that looks like the other pick-ones. A native select in this strip
|
||
draws a tall white well and a chevron of the browser's choosing, which reads
|
||
as a form field among buttons; the app's own idiom for "choose one of these"
|
||
is `label ▾`, and this IS one of those. So: the select keeps the behaviour and
|
||
the keyboard, and borrows the look. The arrow is the wrapper's, because an
|
||
option list cannot carry one. */
|
||
.pick { position: relative; display: inline-flex; align-items: center; }
|
||
.pick > select {
|
||
appearance: none;
|
||
background: var(--pane);
|
||
padding: 1px 15px 1px 6px;
|
||
cursor: pointer;
|
||
}
|
||
.pick > select:hover { background: #fff; }
|
||
.pick::after {
|
||
content: "▾";
|
||
position: absolute;
|
||
right: 5px;
|
||
color: var(--dim);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.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); }
|
||
|
||
/* Buttons that are one control: a transport, a stepper, a mode picker. They
|
||
share their borders, so the group reads as a single object with parts rather
|
||
than as several things that happen to be adjacent — which is the whole claim
|
||
a segmented control makes, and the reason `timeline` and `cel sheet` are one
|
||
of these. `.seg` is `.group` with that meaning; they are drawn the same
|
||
because the difference is what the buttons do, not how they look.
|
||
|
||
The negative margin collapses the doubled border between two buttons into
|
||
one hairline. A pressed button then has to be raised, or the neighbour's
|
||
plain border covers half of its accent one. */
|
||
.group, .seg { display: flex; align-items: center; }
|
||
.group > button, .seg > button { border-radius: 0; margin-left: -1px; }
|
||
.group > button:first-child, .seg > button:first-child { margin-left: 0; border-radius: 2px 0 0 2px; }
|
||
.group > button:last-child, .seg > button:last-child { border-radius: 0 2px 2px 0; }
|
||
.group > button.on, .seg > button.on { position: relative; z-index: 1; }
|
||
|
||
/* The rule between two groups of unrelated controls. Shorter than the strip, so
|
||
it reads as a division of the row rather than as a wall across it. */
|
||
.sep { flex: 0 0 1px; width: 1px; height: 13px; background: var(--line); }
|
||
|
||
/* An icon button. The glyph is inked in `currentColor`, so `.on` colours it
|
||
with the same rule that colours a word. 11px to sit on 11px type. */
|
||
button.ico { padding: 2px 5px; line-height: 0; }
|
||
button.ico > svg { display: block; width: 11px; height: 11px; }
|
||
/* Play is the one control in the strip you aim at without looking. */
|
||
button.ico-play { padding-left: 8px; padding-right: 8px; }
|
||
|
||
/* `hold −` / `hold +`: one label over two steppers, because the word is shared
|
||
and repeating it in both buttons was most of their width. */
|
||
.stepper { display: flex; align-items: center; gap: 4px; }
|
||
.stepper-label { color: var(--dim); }
|
||
.stepper button { padding: 1px 6px; }
|
||
|
||
/* A number that changes every frame. Tabular figures stop it twitching, and
|
||
stop the controls after it being nudged about as the count passes 9 and 99. */
|
||
.readout { font-variant-numeric: tabular-nums; }
|
||
|
||
/* The answer to a refusal rather than a standing command — `lane-retry` only
|
||
exists because something was declined and named this as the way through. */
|
||
button.retry { border-color: var(--sel); color: var(--sel); }
|
||
|
||
/* --------------------------------------------------------------------------
|
||
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);
|
||
}
|
||
|
||
.project-title { max-width: 22vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.project-name { width: 180px; }
|
||
.export-scrim { position: fixed; inset: 0; z-index: 50; background: rgba(0, 0, 0, .45); }
|
||
.export-dialog {
|
||
position: fixed; z-index: 51; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||
width: min(360px, calc(100vw - 32px)); padding: 14px;
|
||
background: var(--pane); border: 1px solid var(--line); border-radius: 3px;
|
||
box-shadow: 0 8px 30px rgba(0, 0, 0, .45);
|
||
}
|
||
.export-dialog h2 { margin: 0 0 12px; font: inherit; font-weight: 600; }
|
||
.export-field { display: grid; gap: 4px; margin: 8px 0; color: var(--dim); }
|
||
.export-field select { color: var(--fg); }
|
||
|
||
/* --------------------------------------------------------------------------
|
||
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); }
|
||
|
||
/* Rows about the document rather than choices in it: who can edit. */
|
||
.menu-item.static { display: flex; align-items: center; gap: 6px; cursor: default; }
|
||
.menu-item.static:hover { background: none; }
|
||
.menu-item.static .sub { display: inline; margin-left: auto; }
|
||
.menu-item.static button { margin-left: auto; }
|
||
.menu .row { margin: 4px 0; flex-wrap: nowrap; }
|
||
.menu .row input { flex: 1; min-width: 0; }
|
||
.menu p { margin: 4px 0; }
|
||
.account { display: grid; gap: 4px; }
|
||
button.link { border: 0; background: none; padding: 0; color: var(--sel); cursor: pointer; }
|
||
|
||
/* `/`: your projects, over the editor. */
|
||
.index { position: fixed; inset: 0; z-index: 30; display: flex; flex-direction: column;
|
||
background: var(--pane); }
|
||
.index-body { width: min(640px, 100%); margin: 0 auto; padding: 24px 16px; overflow: auto; }
|
||
.index-body h1 { font: inherit; font-size: 18px; font-weight: 600; margin: 0 0 12px; }
|
||
.index-head { display: flex; align-items: baseline; justify-content: space-between; }
|
||
.index-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--hair); }
|
||
.index-list li { display: flex; justify-content: space-between; gap: 12px;
|
||
padding: 7px 2px; border-bottom: 1px solid var(--hair); }
|
||
.index-list a { color: var(--fg); text-decoration: none; font-weight: 600; }
|
||
.index-list a:hover { color: var(--sel); }
|
||
a.brand { text-decoration: none; }
|
||
|
||
/* Undo, its list, redo: one group. */
|
||
.undo { display: flex; gap: 0; }
|
||
.undo > button { border-radius: 0; }
|
||
.undo > button:first-child { border-radius: 2px 0 0 2px; }
|
||
.undo > .undo-list { padding: 0 4px; margin-right: 4px; border-left: 0; border-radius: 0 2px 2px 0; }
|
||
.menu.menu-left { left: 0; right: auto; }
|
||
|
||
/* A menu dropping out of a PANE HEAD. Fixed and placed by `ui/menu` against the
|
||
button, because a pane clips its own overflow and an absolutely positioned
|
||
panel would be cut off at the pane's edge — invisibly, and more of it the
|
||
longer the menu gets. `top`/`bottom`/`left`/`max-height` all come from there.
|
||
|
||
Wide enough that a command's one-line explanation is one line. At 215px every
|
||
`.sub` in the cel menu wrapped, which doubled the panel's height for no gain:
|
||
the explanations are the reason these are menu rows and not buttons, so they
|
||
get the width to be read in. */
|
||
.menu.menu-drop {
|
||
position: fixed;
|
||
/* BOTH offsets on each axis are reset, and `ui/menu` sets exactly one of each
|
||
inline. Leaving `.menu`'s own `top` standing alongside an inline `bottom`
|
||
over-constrains the box, and a fixed box given both offsets takes its HEIGHT
|
||
from them — which, from a toolbar near the foot of the window, computes
|
||
negative and collapses the panel to its padding with the commands still
|
||
inside it. */
|
||
top: auto;
|
||
bottom: auto;
|
||
left: auto;
|
||
right: auto;
|
||
width: 272px;
|
||
overflow: auto;
|
||
}
|
||
.menu-note { padding: 1px 6px 5px; }
|
||
|
||
/* A disabled row still says what the command is FOR. That is the whole trade:
|
||
a greyed button hides its reason in a `title`, a greyed row prints it. */
|
||
.menu-item:disabled { opacity: .5; cursor: default; }
|
||
.menu-item:disabled:hover { background: none; }
|
||
.menu-item { padding: 3px 6px; }
|
||
.menu-item .sub { font-size: 10px; line-height: 1.3; white-space: normal; }
|
||
|
||
/* The one way to hand the document to somebody else, so it looks like one. */
|
||
button.share-button { background: var(--sel); color: #fff; border-color: var(--sel); font-weight: 600; padding: 1px 12px; }
|
||
button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
||
|
||
/* Who else has this document open. */
|
||
.roster { display: flex; gap: 2px; align-items: center; }
|
||
.peer {
|
||
width: 18px; height: 18px; border-radius: 50%;
|
||
display: inline-grid; place-items: center;
|
||
background: var(--sel); color: #fff; font-size: 10px; font-weight: 600;
|
||
}
|
||
.peer.guest { background: var(--dim); }
|
||
|
||
/* --------------------------------------------------------------------------
|
||
media pool
|
||
|
||
A FILE LIST, AND IT IS 210px WIDE. Every row is one line — a picture at the
|
||
stage's own proportions, a name, one number — because the pane under this one
|
||
is the timeline and a row that costs two lines costs timeline rows. The row is
|
||
--row tall, which is a timeline row, so a pool of symbols and the rows they
|
||
end up on are the same size object.
|
||
|
||
One exception, deliberately: the main timeline. See `.pool-row.main`. */
|
||
|
||
/* The search strip, between the pane head and the tabs. Its own band rather
|
||
than crowded into the head, which is one line and already holds the + . */
|
||
.pool-find {
|
||
position: sticky;
|
||
top: 21px;
|
||
z-index: 2;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 3px 5px;
|
||
background: var(--chrome);
|
||
border-bottom: 1px solid var(--hair);
|
||
}
|
||
|
||
.pool-search {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 17px;
|
||
padding: 0 5px;
|
||
font: inherit;
|
||
color: var(--fg);
|
||
background: var(--pane);
|
||
border: 1px solid var(--line);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
/* Safari draws its own clear button inside a search input, in its own idiom and
|
||
at its own size. The one next to it is this file's. */
|
||
.pool-search::-webkit-search-decoration,
|
||
.pool-search::-webkit-search-cancel-button { -webkit-appearance: none; }
|
||
|
||
.pool-clear {
|
||
flex: 0 0 auto;
|
||
width: 17px;
|
||
height: 17px;
|
||
padding: 0;
|
||
border: 0;
|
||
border-radius: 2px;
|
||
background: none;
|
||
color: var(--dim);
|
||
cursor: pointer;
|
||
}
|
||
.pool-clear:hover { background: var(--hair); color: var(--fg); }
|
||
|
||
/* The scope tabs, in the vocabulary `ui/tabs` already established for symbols —
|
||
the same object, so two rows of tabs on one screen read as one idea. */
|
||
.pool > .tabs { top: 45px; position: sticky; z-index: 2; }
|
||
|
||
/* A tally, shown only on the tab you are NOT looking at and only while a search
|
||
is running: it says "what you asked for is over here". */
|
||
.tab .count {
|
||
padding: 0 4px;
|
||
border-radius: 7px;
|
||
background: var(--sel-bg);
|
||
color: var(--sel);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* MEDIA, SOUNDS, TIMELINES. A heading is one line and it sticks, so scrolling a
|
||
long list never leaves you looking at rows with no idea what kind they are. */
|
||
.pool-section { margin-bottom: 7px; }
|
||
|
||
.pool-section > h2 {
|
||
position: sticky;
|
||
top: 69px;
|
||
z-index: 1;
|
||
margin: 0 0 2px;
|
||
padding: 1px 5px;
|
||
font: inherit;
|
||
font-weight: 600;
|
||
color: var(--dim);
|
||
text-transform: uppercase;
|
||
letter-spacing: .07em;
|
||
background: var(--pane);
|
||
}
|
||
|
||
.pool-section > .dim { padding: 0 5px 2px; }
|
||
|
||
.pool-rows { display: flex; flex-direction: column; }
|
||
|
||
/* What a search found nothing to say. */
|
||
.pool-empty { padding: 6px 5px; color: var(--dim); }
|
||
|
||
.pool-project { margin: 0 0 1px 5px; }
|
||
.pool-project > summary { cursor: pointer; color: var(--dim); padding: 1px 0; }
|
||
.pool-project > .pool-rows { margin-left: 8px; }
|
||
|
||
/* THE ROW IS THE BUTTON. Click selects, double-click opens, and drag starts
|
||
anywhere across it — the whole strip, not the few characters of the name. The
|
||
wrapper carries the state classes and the frame; the button inside it fills
|
||
that frame edge to edge, so there is no dead strip anywhere along the row. */
|
||
.pool-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
height: var(--row);
|
||
padding: 0 4px;
|
||
border: 1px solid transparent;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.pool-row:hover:not(.disabled) { background: #fff; }
|
||
.pool-row.on { background: var(--sel-bg); border-color: var(--sel); }
|
||
|
||
/* WHICH ONE IS OPEN, as a mark and not as the word "open" — a list this narrow
|
||
cannot spend twenty pixels of every name on a word that is true of one row.
|
||
The accent, because selection is the only thing it is ever used for and the
|
||
open symbol is what the rest of the screen is showing. */
|
||
.pool-row.open { box-shadow: inset 2px 0 0 var(--sel); }
|
||
|
||
.pool-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 0;
|
||
text-align: left;
|
||
border: 0;
|
||
background: none;
|
||
color: inherit;
|
||
font: inherit;
|
||
cursor: grab;
|
||
}
|
||
|
||
/* Shrinks to the name, so the pencil sits against the end of the word rather
|
||
than out at the edge of the pane — and ellipses rather than pushing the
|
||
number off the row. */
|
||
.pool-item .text { display: flex; flex-direction: column; flex: 0 1 auto; min-width: 0; }
|
||
.pool-item .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
|
||
/* The number, at the far edge and never moving: `auto` on the left is what
|
||
pushes it there, and tabular figures are what stop it shuffling as the rows
|
||
scroll past. */
|
||
.pool-item > .sub {
|
||
flex: 0 0 auto;
|
||
margin-left: auto;
|
||
padding-left: 4px;
|
||
color: var(--dim);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* Every row leads with a picture, and they are all this box, so the names line
|
||
up down the list whatever is beside them. 16:10 is the stage's own ratio at
|
||
320x200 — a thumbnail that is not the picture's shape is a thumbnail you have
|
||
to think about. */
|
||
.pool-item .thumb {
|
||
flex: 0 0 32px;
|
||
width: 32px;
|
||
height: 20px;
|
||
object-fit: cover;
|
||
background: var(--stage);
|
||
border-radius: 1px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.pool-item .thumb.sound {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #d9d9d9;
|
||
}
|
||
|
||
/* THE MAIN TIMELINE, under the PROJECT heading and at the top of the pane.
|
||
`clip/opens-on`'s answer: the longest symbol nothing else places, which is
|
||
where the work happens. The one row in the pane given size, a second line and
|
||
a surface of its own — spent here rather than spread over every row, which is
|
||
what keeps the list below it a list. */
|
||
.pool-row.main {
|
||
height: auto;
|
||
margin-bottom: 5px;
|
||
padding: 4px;
|
||
background: var(--sunk);
|
||
border-color: var(--hair);
|
||
}
|
||
|
||
.pool-row.main .thumb {
|
||
flex: 0 0 64px;
|
||
width: 64px;
|
||
height: 40px;
|
||
}
|
||
|
||
.pool-row.main .name { font-weight: 600; }
|
||
|
||
/* Both facts on one line under the name. Beside a 64px picture there is room,
|
||
and this is the one row where a person wants more than the length before
|
||
they click. */
|
||
.pool-row.main .detail {
|
||
color: var(--dim);
|
||
font-variant-numeric: tabular-nums;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* The pencil. A span and not a button — see `ui/pool`'s `row` — so it needs its
|
||
own centring and cursor rather than inheriting a control's.
|
||
|
||
Hidden until the row is under the pointer or holds focus, because a column of
|
||
twenty pencils is a column of twenty pencils; `visibility` rather than
|
||
`display`, so it keeps its width and the name beside it does not reflow as the
|
||
pointer crosses the list. */
|
||
.pool-rename {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 0 0 15px;
|
||
width: 15px;
|
||
height: 15px;
|
||
border-radius: 2px;
|
||
color: var(--dim);
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
visibility: hidden;
|
||
}
|
||
|
||
.pool-row:hover .pool-rename,
|
||
.pool-row:focus-within .pool-rename { visibility: visible; }
|
||
.pool-rename:hover { background: var(--hair); color: var(--fg); }
|
||
|
||
/* Renaming. The input REPLACES the row at the same height, so a list does not
|
||
jump while you type in it. */
|
||
.pool-name {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 17px;
|
||
padding: 0 4px;
|
||
font: inherit;
|
||
color: var(--fg);
|
||
background: #fff;
|
||
border: 1px solid var(--sel);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
/* 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); }
|
||
|
||
.trace-opacity { width: 64px; }
|
||
.palette-bar label.dim { display: inline-flex; align-items: center; gap: 2px; white-space: nowrap; }
|
||
|
||
.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; }
|
||
/* The footage traced over: a reference above the picture, never part of it. */
|
||
.underlay { position: absolute; inset: 0; pointer-events: 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;
|
||
}
|
||
|
||
.inspector-form { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; margin-top: 7px; }
|
||
.inspector-field { display: grid; gap: 2px; color: var(--dim); }
|
||
.inspector-field input { width: 100%; color: var(--fg); }
|
||
|
||
.correction-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
|
||
.correction-grid select, .correction-grid input { width: 100%; color: var(--fg); }
|
||
.correction-list, .correction-conflicts { display: grid; gap: 4px; margin-top: 7px; }
|
||
.correction-item { display: flex; align-items: center; gap: 4px; min-width: 0; }
|
||
.correction-item > span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* --------------------------------------------------------------------------
|
||
the location bar
|
||
|
||
Where you are, above the timeline that draws it. Chrome-coloured like a pane
|
||
head, because that is what it is to the two temporal views below it — but it
|
||
is not one of their heads, since it says the same thing whichever is showing. */
|
||
|
||
.loc {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
padding: 0 7px;
|
||
min-width: 0;
|
||
background: var(--chrome);
|
||
color: var(--dim);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.crumbs { display: flex; align-items: center; gap: 1px; min-width: 0; overflow: hidden; }
|
||
|
||
/* A crumb is a place, not a command: no border and no fill until it is pointed
|
||
at. The trail has to read as one sentence, and five outlined buttons in a row
|
||
read as five things to press. */
|
||
.crumb {
|
||
flex: 0 1 auto;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
padding: 1px 4px;
|
||
border: 1px solid transparent;
|
||
border-radius: 2px;
|
||
background: none;
|
||
color: var(--dim);
|
||
}
|
||
.crumb:hover:not(:disabled) { background: var(--sel-bg); color: var(--fg); }
|
||
/* The last crumb is the selection itself. Weight and full contrast say so; the
|
||
fill and accent border `button.on` would otherwise give it are undone here,
|
||
because a filled crumb reads as a pressed control and the thing it marks is
|
||
where you ARE, not something switched on. */
|
||
.crumb.on { color: var(--fg); font-weight: 600; background: none; border-color: transparent; }
|
||
.crumb.lane::before { content: "≡ "; color: var(--dim); font-weight: 400; }
|
||
.crumb-sep { flex: 0 0 auto; color: var(--line); }
|
||
|
||
.loc-fact { flex: 0 0 auto; }
|
||
.loc-fact::before, .loc-shared::before { content: "· "; color: var(--line); }
|
||
.loc-shared { flex: 0 0 auto; display: inline-flex; align-items: baseline; gap: 5px; }
|
||
.loc .spacer { flex: 1; }
|
||
|
||
/* --------------------------------------------------------------------------
|
||
timeline */
|
||
|
||
.time { overflow: hidden; }
|
||
|
||
.tl-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
/* Not `stretch`: that sizes both columns to the body's height, the rows then
|
||
overflow them, and the sticky ruler goes out with its column. Each column is
|
||
as tall as its rows instead, and at least as tall as the body. */
|
||
align-items: flex-start;
|
||
overflow: auto;
|
||
}
|
||
|
||
.tl-labels,
|
||
.tl-tracks { min-height: 100%; }
|
||
|
||
.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;
|
||
/* Over the ruler and the playhead, which both pass under it when the tracks
|
||
scroll sideways. */
|
||
z-index: 4;
|
||
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 {
|
||
/* Scrolled to from the stage: clear of the sticky ruler above. */
|
||
scroll-margin-top: var(--ruler);
|
||
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-delete { margin-left: auto; padding: 0 4px; border: 0; background: none; color: var(--dim); }
|
||
.tl-delete:hover { color: var(--fg); }
|
||
.tl-solo, .tl-trace { margin-left: auto; padding: 0 4px; border: 0; background: none; color: var(--dim); font-size: 10px; }
|
||
.tl-solo:hover, .tl-trace:hover { color: var(--fg); }
|
||
.tl-solo.on, .tl-trace.on { color: var(--sel); font-weight: 600; }
|
||
.tl-solo + .tl-delete, .tl-trace + .tl-solo { margin-left: 0; }
|
||
|
||
/* The ruler and the corner above the labels stay at the top of the body while
|
||
the rows scroll under them: the ruler is the scrubber, and scrolling down to
|
||
a row is no reason to lose it. */
|
||
.tl-corner,
|
||
.tl-ruler {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 2;
|
||
height: var(--ruler);
|
||
border-bottom: 1px solid var(--line);
|
||
background: var(--chrome);
|
||
}
|
||
|
||
.tl-ruler {
|
||
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 heading between the picture's rows and the sounds', as an editor puts
|
||
its audio tracks under its video tracks. */
|
||
.tl-label.tl-section,
|
||
.tl-track.tl-section { background: var(--chrome); border-top: 1px solid var(--line); }
|
||
.tl-label.tl-section { padding-left: 6px; color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; }
|
||
|
||
/* A sound's bar, told apart from the picture's at a glance. */
|
||
.tl-span.sound { background: #e3efdf; border-color: #a6c49b; }
|
||
.pool-item .thumb.sound { display: flex; align-items: center; justify-content: center; color: #d9d9d9; }
|
||
|
||
/* 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; }
|
||
|
||
/* A row being dragged over another: into an instance, or grouped with a node,
|
||
by its middle; in front of it or behind it, by its top or bottom edge. */
|
||
.tl-label.drop-into { box-shadow: inset 0 0 0 2px var(--sel); background: var(--sel-bg); }
|
||
.tl-label.drop-group { outline: 2px dashed var(--sel); outline-offset: -2px; }
|
||
.tl-label.drop-front { box-shadow: inset 0 2px 0 var(--sel); }
|
||
.tl-label.drop-back { box-shadow: inset 0 -2px 0 var(--sel); }
|
||
|
||
/* A node's bar slides it along its symbol's time. */
|
||
.tl-span.movable { cursor: ew-resize; touch-action: none; }
|
||
.tl-span.movable.sliding { border-color: var(--sel); }
|
||
.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%;
|
||
/* So grabbing a dot grabs the bar under it. */
|
||
pointer-events: none;
|
||
}
|
||
|
||
.tl-playhead {
|
||
position: absolute;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 1px;
|
||
background: var(--playhead);
|
||
pointer-events: none;
|
||
z-index: 3;
|
||
}
|
||
|
||
/* The playhead's head, in the ruler rather than on the line, so it stays at the
|
||
top with the ruler while the rows scroll. */
|
||
.tl-knob {
|
||
position: absolute;
|
||
pointer-events: none;
|
||
margin-left: -4px;
|
||
top: 1px;
|
||
width: 9px;
|
||
height: 9px;
|
||
border-radius: 50%;
|
||
background: var(--playhead);
|
||
}
|
||
|
||
.tl-empty { padding: 9px; color: var(--dim); }
|
||
|
||
/* The second temporal view. Its cells carry the same selection addresses as
|
||
the cel blocks above; only the axes change. */
|
||
.cel-sheet {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
display: grid;
|
||
align-content: start;
|
||
background: var(--line);
|
||
gap: 1px;
|
||
}
|
||
|
||
.cs-head,
|
||
.cs-frame,
|
||
.cs-cell {
|
||
min-width: 0;
|
||
height: 24px;
|
||
border: 0;
|
||
border-radius: 0;
|
||
padding: 0 6px;
|
||
background: #fff;
|
||
color: var(--fg);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.cs-head {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 2;
|
||
display: flex;
|
||
align-items: center;
|
||
background: var(--chrome);
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.cs-head.cs-frame { z-index: 3; }
|
||
.cs-frame { position: sticky; left: 0; z-index: 1; color: var(--dim); text-align: right; }
|
||
.cs-frame.on, .cs-cell.current { box-shadow: inset 3px 0 0 var(--playhead); }
|
||
.cs-cell { text-align: left; cursor: pointer; }
|
||
.cs-cell:hover { background: var(--sel-bg); }
|
||
.cs-cell.selected { background: var(--sel-bg); color: var(--sel); font-weight: 600; }
|
||
.cs-head.selected { background: var(--sel-bg); color: var(--sel); }
|
||
.cel-sheet { user-select: none; touch-action: none; }
|
||
.cs-cell { position: relative; }
|
||
.cs-cell:focus-visible { outline: 2px solid var(--sel); outline-offset: -2px; }
|
||
.cs-help { padding: 6px 10px; color: var(--dim); font-size: 11px;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.cs-fill-handle { position: absolute; right: 0; bottom: 0; width: 9px; height: 9px;
|
||
background: var(--sel); border: 1px solid white; cursor: ns-resize; z-index: 2; }
|
||
.cs-preview { outline: 1px dashed var(--sel); outline-offset: -1px; }
|
||
|
||
/* --------------------------------------------------------------------------
|
||
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);
|
||
}
|
||
|
||
/* A transform or visibility channel: its key button, then a field per component. */
|
||
.facts dd.channel { display: flex; gap: 3px; align-items: center; overflow: visible; }
|
||
.facts dd.channel input[type=number] { width: 0; flex: 1; min-width: 0; }
|
||
.facts dd.channel .key { padding: 0 3px; color: var(--dim); }
|
||
.facts dd.channel .key.keyed { color: var(--fg); }
|
||
.facts dd.channel .key.on { color: var(--sel); }
|
||
|
||
/* The selected node's transform box, in stage pixels (the SVG's viewBox). */
|
||
.paint-overlay .handles .box { fill: none; stroke: #e6ca8b; stroke-width: 0.5; stroke-dasharray: 2 1; pointer-events: none; }
|
||
.paint-overlay .handles .knob-arm { stroke: #e6ca8b; stroke-width: 0.5; pointer-events: none; }
|
||
.paint-overlay .handles .knob { fill: #161820; stroke: #fff1be; stroke-width: 0.6; cursor: grab; }
|
||
.paint-overlay .handles .corner { fill: #fff1be; stroke: #161820; stroke-width: 0.5; cursor: nwse-resize; }
|
||
.paint-overlay .handles .pivot { stroke: #fff1be; stroke-width: 0.6; pointer-events: none; }
|
||
.paint-overlay:focus { outline: none; }
|