1269 lines
41 KiB
CSS
1269 lines
41 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 SECOND accent, and the one exception to the rule above — stated here
|
|
because SELECTED and AIMED are two different facts that are often true of
|
|
one node at the same time, and one accent cannot say both.
|
|
Selected is what you are LOOKING at: the inspector's subject, the thing
|
|
with handles on it. Aimed is where a new drawing or symbol would GO. They
|
|
used to be the same state, which is how drawing a polygon ended up in
|
|
whatever you had last clicked on the stage.
|
|
Violet rather than another blue: in the chrome it has to be told apart
|
|
from --sel, and on the stage from the warm gold the handles are drawn in.
|
|
Colour is not the only channel either — the aim outline is SOLID where the
|
|
selection's box is dashed, and it carries no handles at all, because there
|
|
is nothing about being aimed at that you could drag. --aim-stage is the
|
|
same hue lifted to read against --stage, which is the one dark surface. */
|
|
--aim: #8d4bd6;
|
|
--aim-stage: #c79bf2;
|
|
|
|
/* Auto-key is a recording state, distinct from selection and aiming. */
|
|
--live: #239447;
|
|
--live-bg: #e2f5e7;
|
|
|
|
/* 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. `.seg` is `.group` with that meaning; both are drawn
|
|
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; }
|
|
|
|
/* 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); }
|
|
|
|
.palette-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 9px;
|
|
padding: 4px 8px;
|
|
background: var(--chrome);
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.palette-bar .auto-key.on {
|
|
color: #145c2b;
|
|
background: var(--live-bg);
|
|
border-color: var(--live);
|
|
box-shadow: 0 0 7px rgba(35, 148, 71, .72), inset 0 0 0 1px rgba(255, 255, 255, .7);
|
|
}
|
|
|
|
.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; }
|
|
|
|
/* The footage slider shares its row with the word "opacity", so it takes what is
|
|
left rather than the full width the generic range rule gives it. */
|
|
.trace-opacity { flex: 1; min-width: 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.aimed { box-shadow: inset 0 0 0 2px var(--aim); }
|
|
.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-edge {
|
|
position: absolute;
|
|
top: -1px;
|
|
bottom: -1px;
|
|
width: 7px;
|
|
cursor: e-resize;
|
|
touch-action: none;
|
|
z-index: 2;
|
|
}
|
|
.tl-edge.out { right: -3px; }
|
|
.tl-edge.in { left: -3px; }
|
|
.tl-junction {
|
|
position: absolute;
|
|
top: -1px;
|
|
left: 0;
|
|
bottom: -1px;
|
|
width: 12px;
|
|
transform: translateX(-50%);
|
|
cursor: col-resize;
|
|
touch-action: none;
|
|
z-index: 3;
|
|
}
|
|
.tl-cel-label { display: block; overflow: hidden; text-overflow: ellipsis; 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%;
|
|
/* 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 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); }
|
|
.facts dd.channel.live {
|
|
margin: -2px;
|
|
padding: 2px;
|
|
border-radius: 3px;
|
|
background: var(--live-bg);
|
|
box-shadow: 0 0 0 1px rgba(35, 148, 71, .45), 0 0 6px rgba(35, 148, 71, .28);
|
|
}
|
|
.facts dd.channel.live input { border-color: var(--live); }
|
|
|
|
/* 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; }
|
|
|
|
/* The aim outline and its name tag. Solid, no handles, and a tag pinned just
|
|
outside the bottom-right corner — see `ui/stage/aim-box` for why the box
|
|
rotates with the node and the tag does not. Sizes are in user units because
|
|
this SVG's viewBox is the stage's 320x200 scaled by `zoom`. */
|
|
.paint-overlay .aim { pointer-events: none; }
|
|
.paint-overlay .aim .aim-box { fill: none; stroke: var(--aim-stage); stroke-width: 0.7; }
|
|
.paint-overlay .aim .aim-tag-bg { fill: var(--aim-stage); }
|
|
.paint-overlay .aim .aim-tag {
|
|
fill: #1b1030;
|
|
font: 600 3.4px ui-sans-serif, system-ui, sans-serif;
|
|
letter-spacing: 0.04px;
|
|
}
|
|
.paint-overlay:focus { outline: none; }
|