A peg was reachable and not usable. It is a `:group`, so it draws nothing, and both of the stage's ways in key off a DRAWN op: `pick/choose` hit-tests the ops, so a click never found it, and `handles` hangs its box, corners and turn knob off `pick/bounds-of`, so it got only the pivot cross — which is `pointer-events: none`. So the pivot `gesture/refusal` tells you to make could be made, and then only typed at in the inspector, and reselected only from a timeline row. Every assertion about the maths passed throughout, which is the point: nothing under `domain/` can see this. So the handles fall back to a fixed-size rosette about the peg's own origin — cross to move, knob to turn, four corners to scale. Fixed size, in stage pixels, because there is no drawing for it to be proportional to. The cross gets a transparent `.peg-grab` disc rather than taking the handler itself, since `.pivot` must stay `pointer-events: none` everywhere else: it is a mark on the picture and must not eat a click meant for the shape under it. `test/browser/peg.mjs` asserts the handles as DOM and drags through them with real pointer events, because that is the half a unit test cannot reach: a peg appears with no drift at all, is selected, renders one knob, four corners and no box, and dragging its cross moves its child by exactly the drag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HkinzDz1VtahZVsujAGRBD
1813 lines
64 KiB
CSS
1813 lines
64 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 because selection and effective creation target are two
|
|
facts that are often true of one node at the same time.
|
|
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 target outline is SOLID where the
|
|
selection's box is dashed, and it carries no handles at all, because there
|
|
is nothing about being a creation parent that you could drag. --target-stage is the
|
|
same hue lifted to read against --stage, which is the one dark surface. */
|
|
--target: #8d4bd6;
|
|
--target-stage: #c79bf2;
|
|
|
|
/* A THIRD, and the last. SELECTED IN THE TIMELINE had been --sel-bg, which is
|
|
#cfe0f5 against spans drawn in #dfe8f4: two pale blues a hair apart, in the
|
|
one place where which row and which clip is selected decides what the pane
|
|
is showing — an expanded lane opens exactly the selected clip. The fix is
|
|
not a stronger blue, which stays in the family the spans already own, but a
|
|
different hue entirely. Ochre: warm against every cool surface in the pane,
|
|
clear of --target's violet, --live's green and the playhead's red, and dark
|
|
enough at full strength to carry white type on a 21px block. */
|
|
--pick: #a85c00;
|
|
--pick-bg: #ffd9a0;
|
|
--pick-fg: #4a2800;
|
|
|
|
/* Auto-key is a recording state, distinct from selection and creation target. */
|
|
--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 */
|
|
|
|
/* THE TRACKS ARE CUSTOM PROPERTIES, NOT LENGTHS. `ui/layout` writes all six on
|
|
this element, which is what makes the three edges between the panes draggable:
|
|
a drag is one `assoc-in` and one property, and no pane's contents re-render.
|
|
The defaults here are the same numbers `ui/layout`'s `normal` holds, written
|
|
twice so that this file still describes a window on its own. */
|
|
.app {
|
|
display: grid;
|
|
height: 100%;
|
|
--pool-w: 210px;
|
|
--params-w: 250px;
|
|
--time-h: 232px;
|
|
--grip-pool: 4px;
|
|
--grip-params: 4px;
|
|
--grip-time: 4px;
|
|
grid-template-columns:
|
|
var(--pool-w) var(--grip-pool) minmax(0, 1fr) var(--grip-params) var(--params-w);
|
|
/* 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 var(--grip-time) var(--time-h);
|
|
grid-template-areas:
|
|
"top top top top top"
|
|
"pool gpool view gparm params"
|
|
"loc loc loc loc loc"
|
|
"gtime gtime gtime gtime gtime"
|
|
"time time 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; }
|
|
|
|
/* The draggable edge between two panes. Four pixels of chrome that lights up
|
|
under the pointer: a hairline is honest about where the boundary is and
|
|
impossible to hit, and a wide gutter would be furniture. */
|
|
.grip { touch-action: none; background: var(--chrome); }
|
|
.grip:hover, .grip:active { background: var(--sel); }
|
|
.grip.col { cursor: col-resize; }
|
|
.grip.row { cursor: row-resize; }
|
|
.grip-pool { grid-area: gpool; }
|
|
.grip-params { grid-area: gparm; }
|
|
.grip-time { grid-area: gtime; }
|
|
|
|
/* Which panes are open, in the top bar. Lower-case words rather than icons: the
|
|
three panes have names and the names are what the rest of the chrome calls
|
|
them. */
|
|
.panes > button { color: var(--dim); }
|
|
.panes > button.on { color: var(--fg); }
|
|
|
|
/* A zoom readout that is also the way back to 1:1, so it is a button that looks
|
|
like the number it shows. Tabular figures, because the + beside it must not
|
|
move as the percentage passes 100. */
|
|
.zoom-at { min-width: 44px; text-align: center; font-variant-numeric: tabular-nums; }
|
|
|
|
/* 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); }
|
|
|
|
.options-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 9px;
|
|
padding: 4px 8px;
|
|
background: var(--chrome);
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.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; }
|
|
.palette-slot { display: flex; align-items: center; }
|
|
.palette-picker { display: none; }
|
|
.thumb i { display: block; min-width: 1px; min-height: 1px; }
|
|
|
|
/* 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: flex;
|
|
flex-direction: column;
|
|
/* `safe`, because the stage zooms: a centred item larger than its scroll
|
|
container puts its top-left edge out of reach on the start side, which at
|
|
8x is most of the picture. Safe alignment falls back to start when it does
|
|
not fit, and centring is only ever about where a SMALL stage sits. */
|
|
align-items: safe center;
|
|
justify-content: safe center;
|
|
overflow: auto;
|
|
padding: 14px;
|
|
}
|
|
|
|
.stage-target {
|
|
flex: 0 0 auto;
|
|
margin-top: -1px;
|
|
padding: 3px 9px 4px;
|
|
border: 1px solid var(--line);
|
|
border-top: 0;
|
|
border-radius: 0 0 3px 3px;
|
|
background: var(--pane);
|
|
color: var(--dim);
|
|
}
|
|
|
|
.stage-target strong { color: var(--fg); font-weight: 600; }
|
|
|
|
/* 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; }
|
|
.stage-overlay-wrap {
|
|
position: absolute;
|
|
left: -100%; top: -100%;
|
|
width: 300%; height: 300%;
|
|
}
|
|
.stage-shade {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
outline: 1px solid #aaa;
|
|
}
|
|
/* The footage traced over: a reference above the picture, never part of it. */
|
|
.tracing { position: absolute; inset: 0; pointer-events: none; }
|
|
|
|
/* 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; }
|
|
.creation-controls button { display: inline-flex; align-items: center; gap: 4px; }
|
|
.creation-controls svg { width: 11px; height: 11px; }
|
|
|
|
/* --------------------------------------------------------------------------
|
|
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;
|
|
/* THE GUTTER IS ALWAYS THERE, whether or not there is a scrollbar in it. Every
|
|
mark in this pane is a percentage of the tracks column, and the column is
|
|
what is left of the body after the labels -- so a vertical scrollbar
|
|
appearing took 15px out of the width and moved the ruler, the frame grid,
|
|
every clip and the playhead sideways, mid-gesture. Expanding a lane's portal
|
|
adds seven rows at once, which is exactly when a person is looking at the
|
|
frame they are aiming for. */
|
|
scrollbar-gutter: stable;
|
|
}
|
|
|
|
.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;
|
|
/* The timeline's zoom, and the whole of it: every mark in here is positioned
|
|
as a percentage of this element's width, so one multiplier on the width
|
|
spreads the frame grid, the spans, the keys, the ruler and the playhead
|
|
apart together, and `.tl-body` was already a scroll container. 1 is the
|
|
width that exactly fills the body, which is what `flex: 1` alone gave. */
|
|
min-width: max(340px, calc((100% - var(--label)) * var(--tl-zoom, 1)));
|
|
position: relative;
|
|
background: #fff;
|
|
/* The major frame grid, using the same whole-second interval as the numbered
|
|
ruler. It is a background rather than an element per mark; `--tick` is set
|
|
by the component because only it knows the open symbol's length and FPS. */
|
|
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;
|
|
}
|
|
|
|
/* Selected, in the timeline's own colour. The bar down the left edge is the
|
|
half that survives at a glance: a row 21px tall scrolled past in a column of
|
|
twenty reads its left edge before it reads its fill. */
|
|
.tl-label.on {
|
|
background: var(--pick-bg);
|
|
box-shadow: inset 3px 0 0 var(--pick);
|
|
color: var(--pick-fg);
|
|
}
|
|
.tl-label.on .kind { color: var(--pick); }
|
|
.tl-label.target { box-shadow: inset 0 0 0 2px var(--target); }
|
|
/* Both at once, and they must still be two readable facts: the target ring sits
|
|
inside the pick's edge rather than replacing it. */
|
|
.tl-label.on.target { box-shadow: inset 3px 0 0 var(--pick), inset 0 0 0 2px var(--target); }
|
|
.tl-track.target::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
border: 2px solid var(--target);
|
|
pointer-events: none;
|
|
z-index: 4;
|
|
}
|
|
.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 lane's row: the strip glyph, a faint warm ground under the name, and a
|
|
track behind its blocks — enough that a lane is told from the instance rows
|
|
around it while scanning, and not so much that it shouts. */
|
|
.tl-ico {
|
|
flex: 0 0 12px;
|
|
line-height: 0;
|
|
color: var(--dim);
|
|
}
|
|
.tl-ico > svg { display: block; width: 11px; height: 11px; }
|
|
.tl-label.lane { background: #edeae2; }
|
|
.tl-label.lane .name { font-weight: 600; }
|
|
.tl-label.lane.on { background: var(--pick-bg); }
|
|
.tl-label.lane.on .tl-ico { color: var(--pick); }
|
|
.tl-label.lane:hover:not(.on) { background: #f8f6f0; }
|
|
.tl-track.lane { background: rgba(0, 0, 0, 0.035); }
|
|
.tl-name-input { min-width: 0; flex: 1; font: inherit; }
|
|
.tl-rename { margin-left: auto; padding: 0 3px; border: 0; background: none; color: var(--dim); }
|
|
.tl-rename:hover { color: var(--fg); }
|
|
|
|
/* 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; }
|
|
/* A tracing layer is a reference, never exported: hatched and dashed so it
|
|
cannot be read as a drawing. */
|
|
.tl-span.trace, .tl-cel.trace {
|
|
background: repeating-linear-gradient(135deg, transparent 0 4px, rgba(0, 0, 0, 0.07) 4px 8px);
|
|
border: 1px dashed var(--span-line);
|
|
}
|
|
.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, .tl-cel.ghost { pointer-events: none; }
|
|
.tl-cel.ghost { background: transparent; border: 1px dashed var(--sel); color: var(--sel); }
|
|
|
|
/* 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); }
|
|
|
|
/* Selected: the clip block and the plain bar alike, in the pick colour. A clip
|
|
block had no selected state at all, which is why the portal under an expanded
|
|
lane looked like it opened at random. */
|
|
.tl-cel.on {
|
|
background: var(--pick);
|
|
border-color: var(--pick);
|
|
color: #fff;
|
|
font-weight: 600;
|
|
}
|
|
.tl-cel.on:hover:not(:disabled) { background: var(--pick); filter: brightness(1.12); }
|
|
.tl-span.on { background: var(--pick-bg); border-color: var(--pick); }
|
|
.tl-cel.target, .tl-span.target { outline: 2px solid var(--target); outline-offset: -2px; }
|
|
.tl-span.on.dense {
|
|
background: repeating-linear-gradient(
|
|
-45deg, var(--pick-bg) 0 3px, #f0bf77 3px 6px);
|
|
}
|
|
|
|
/* 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;
|
|
/* Centred on the CUT, which is the clips' shared border-box edge. A clip block
|
|
is a button with a 1px border, so `left: 0` is one pixel inside it and the
|
|
handle sat a pixel to the right of the line it drags. */
|
|
left: -1px;
|
|
bottom: -1px;
|
|
width: 12px;
|
|
transform: translateX(-50%);
|
|
cursor: col-resize;
|
|
touch-action: none;
|
|
z-index: 3;
|
|
}
|
|
/* `nowrap` is load-bearing. A block narrower than its name wrapped it at the
|
|
hyphen — "symbol-" then "8" — and the button is `overflow: visible` so that
|
|
second line spilled out below the track as a row of loose digits under the
|
|
lane, which read as corrupt timing rather than as a long name in a short
|
|
block. One line, clipped. */
|
|
.tl-cel-label {
|
|
display: block;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
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; }
|
|
|
|
/* Passive animation ticks leave the clip underneath available to grab. */
|
|
.tl-key {
|
|
position: absolute;
|
|
top: 50%;
|
|
width: 2px;
|
|
height: 6px;
|
|
margin: -3px 0 0 -1px;
|
|
background: var(--key);
|
|
opacity: 0.5;
|
|
border-radius: 1px;
|
|
/* 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 .channel-swatches {
|
|
display: flex;
|
|
flex: 1;
|
|
flex-wrap: wrap;
|
|
gap: 3px;
|
|
min-width: 0;
|
|
}
|
|
.facts dd.channel .channel-swatches .swatch { flex: 0 0 15px; }
|
|
.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; }
|
|
/* A peg draws nothing, so the cross is the only thing to grab it by. */
|
|
.paint-overlay .handles .peg-grab { fill: transparent; stroke: none; cursor: move; }
|
|
.paint-overlay .marquee { fill: rgba(230, 202, 139, 0.12); stroke: #e6ca8b; stroke-width: 0.6; stroke-dasharray: 2 1; pointer-events: none; }
|
|
|
|
/* The creation-target outline and its name tag. Solid, no handles, and a tag pinned just
|
|
outside the bottom-right corner — see `ui/stage/creation-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 .creation-target { pointer-events: none; }
|
|
.paint-overlay .selected-paint-outline { fill: none; stroke: #fbbf24; stroke-width: 0.8; vector-effect: non-scaling-stroke; pointer-events: none; }
|
|
.paint-overlay .creation-target .creation-target-box { fill: none; stroke: var(--target-stage); stroke-width: 0.7; }
|
|
.paint-overlay .creation-target .creation-target-tag-bg { fill: var(--target-stage); }
|
|
.paint-overlay .creation-target .creation-target-tag {
|
|
fill: #1b1030;
|
|
font: 600 3.4px ui-sans-serif, system-ui, sans-serif;
|
|
letter-spacing: 0.04px;
|
|
}
|
|
.paint-overlay:focus { outline: none; }
|
|
|
|
/* What a drag in flight would do, beside the pointer: a clip body moves in
|
|
time, and with shift it goes inside the clip under the pointer. Said next to
|
|
the cursor because that is where the eye already is mid-gesture. */
|
|
.tl-hint {
|
|
position: fixed;
|
|
z-index: 60;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
background: var(--fg);
|
|
color: var(--bg);
|
|
font-size: 11px;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
}
|
|
.tl-hint.nesting { background: var(--sel); color: #fff; }
|
|
|
|
/* The clip a shift-drag would nest into. Inset, so it reads as "into this"
|
|
rather than as a boundary between rows. */
|
|
.tl-cel.nest-target { outline: 2px solid var(--sel); outline-offset: -2px; }
|
|
|
|
/* A row inside a held clip: shown because the node is on screen for the whole
|
|
hold, dimmed because its own frames have no place on this ruler. */
|
|
.tl-span.unmapped { opacity: 0.45; cursor: pointer; }
|
|
.tl-hint.refusing { background: var(--bad, #b00); color: #fff; }
|
|
|
|
/* --------------------------------------------------------------------------
|
|
narrow windows
|
|
|
|
A phone is not a small desk: there is room for ONE column, so the stage gets
|
|
it and the two side panes become drawers over the top of it rather than
|
|
columns beside it. They are mounted only while open (`ui/shell`), so a shut
|
|
drawer is not holding thumbnails live, and the top bar's pane buttons are
|
|
what opens them — which is why they are at the left end of a strip that
|
|
scrolls.
|
|
|
|
The timeline keeps its row. Shut, that row is its transport strip and
|
|
nothing else, because a window with nowhere to press play is broken rather
|
|
than small; its height is still draggable, by touch, on the grip above it. */
|
|
|
|
@media (max-width: 760px) {
|
|
/* A uuid-wide label column is half the screen. */
|
|
:root { --label: 136px; }
|
|
|
|
.app {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
grid-template-rows: 30px minmax(0, 1fr) 21px var(--grip-time) var(--time-h);
|
|
grid-template-areas:
|
|
"top"
|
|
"view"
|
|
"loc"
|
|
"gtime"
|
|
"time";
|
|
}
|
|
|
|
/* Everything in the strip stays reachable, by scrolling it. */
|
|
.top { overflow-x: auto; }
|
|
.project-title { max-width: 32vw; }
|
|
/* So that 34px below is the same distance on both pages: the editor's strip
|
|
takes its 30px from the grid row, and the index page has no grid. */
|
|
.index > .top { flex: 0 0 30px; }
|
|
|
|
/* A SCROLL CONTAINER CLIPS BOTH AXES. `overflow-x: auto` above makes the
|
|
strip one, so a panel anchored to its own button — open, snapshots, share,
|
|
sign in — is cut off by a 30px-tall bar and drawn at whatever x the strip
|
|
happens to be scrolled to, which reads as the button doing nothing at all.
|
|
Fixed to the viewport instead, as `.menu-drop` already is for the same
|
|
reason one pane down: these are all top-bar menus, the top bar is at the top
|
|
of the window, and a near-full-width sheet is what there is room for. Both
|
|
offsets on the axis, so `.menu-left`'s anchoring is overridden rather than
|
|
left half-applied. `.menu-drop` is excluded because `ui/menu` places that
|
|
one itself, inline, and would then be over-constrained. */
|
|
.top .menu:not(.menu-drop),
|
|
.top .menu.menu-left:not(.menu-drop) {
|
|
position: fixed;
|
|
top: 34px;
|
|
left: 4px;
|
|
right: 4px;
|
|
width: auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* Out of the grid altogether: fixed to the viewport, so the column the grid
|
|
has left is the stage's whether a drawer is open or not. */
|
|
.pool, .params {
|
|
position: fixed;
|
|
z-index: 30;
|
|
top: 31px;
|
|
bottom: 0;
|
|
width: min(78vw, 300px);
|
|
box-shadow: 0 2px 16px rgba(0, 0, 0, .4);
|
|
}
|
|
.pool { left: 0; border-right: 1px solid var(--line); }
|
|
.params { right: 0; border-left: 1px solid var(--line); }
|
|
|
|
/* There is no column edge left to drag. */
|
|
.grip.col { display: none; }
|
|
|
|
/* The picture, not the desk around it. */
|
|
.stage-area { padding: 5px; }
|
|
|
|
/* Touch targets, in the strips that are all buttons. */
|
|
.pane-head, .options-bar { gap: 7px; }
|
|
.options-bar button, .pane-head button, .top button { min-height: 24px; }
|
|
|
|
/* A STRIP OF CONTROLS SCROLLS AS A STRIP. Flexbox's instinct when a row does
|
|
not fit is to shrink every item and then push the last ones off the end —
|
|
and the last ones here are the polygon tool, the timing commands and both
|
|
zoom readouts, which would be unreachable rather than merely off-screen.
|
|
So nothing shrinks, nothing wraps, and each strip scrolls. The `.spacer`
|
|
and `.status` rules above still win on specificity, which is what keeps the
|
|
right-hand groups right-hand while there IS room. */
|
|
.top > *, .pane-head > *, .options-bar > * { flex-shrink: 0; }
|
|
.top button, .pane-head button { white-space: nowrap; }
|
|
.pane-head, .options-bar { overflow-x: auto; }
|
|
/* The hints go first: the tool's name and its controls are what is needed. */
|
|
.options-bar .hint { display: none; }
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
the tools: a strip down the left of the stage, an options bar above it.
|
|
|
|
Photoshop's arrangement, which Illustrator and Flash share. The palette is a
|
|
grid in the strip under the tools, as Deluxe Paint's and Animator Pro's were:
|
|
a fixed table of slots, shown as one. */
|
|
|
|
.options-bar {
|
|
gap: 8px;
|
|
min-height: 29px;
|
|
padding: 3px 8px;
|
|
}
|
|
.options-bar .tool-name { font-weight: 600; min-width: 46px; }
|
|
.options-bar .hint { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 0 1 auto; }
|
|
.options-bar .spacer { flex: 1; }
|
|
.option { display: flex; align-items: center; gap: 6px; color: var(--dim); white-space: nowrap; }
|
|
.option input[type="range"] { width: 110px; }
|
|
.option .value { color: var(--fg); min-width: 30px; font-variant-numeric: tabular-nums; }
|
|
.palette-assets { display: flex; align-items: center; gap: 3px; }
|
|
|
|
.workspace { flex: 1; min-height: 0; display: flex; }
|
|
.workspace > .stage-area { flex: 1; min-width: 0; position: relative; }
|
|
|
|
.toolbox {
|
|
flex: 0 0 52px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 7px 0;
|
|
background: var(--chrome);
|
|
border-right: 1px solid var(--line);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.tools { display: grid; gap: 2px; }
|
|
.tool {
|
|
width: 34px; height: 30px;
|
|
display: grid; place-items: center;
|
|
padding: 0;
|
|
border: 1px solid transparent;
|
|
border-radius: 4px;
|
|
background: none;
|
|
color: var(--fg);
|
|
}
|
|
.tool:hover { background: var(--sunk); border-color: var(--hair); }
|
|
.tool.on { background: var(--sel-bg); border-color: var(--sel); color: var(--sel); }
|
|
.tool svg { fill: currentColor; stroke: none; }
|
|
.tool svg .cut { fill: none; stroke: var(--chrome); stroke-width: 1.2; }
|
|
.tool.on svg .cut { stroke: var(--sel-bg); }
|
|
|
|
.palette { display: grid; gap: 7px; justify-items: center; padding-top: 9px; border-top: 1px solid var(--hair); }
|
|
|
|
/* The colour a new shape gets: Photoshop's foreground chip. */
|
|
.palette .chip {
|
|
width: 36px; height: 36px;
|
|
display: grid; place-items: end start;
|
|
border: 1px solid var(--fg);
|
|
border-radius: 3px;
|
|
box-shadow: inset 0 0 0 2px var(--pane);
|
|
}
|
|
.palette .chip span {
|
|
margin: 0 0 2px 3px; padding: 0 3px;
|
|
border-radius: 2px;
|
|
background: rgba(255, 255, 255, .85);
|
|
color: var(--fg); font-size: 9px; line-height: 12px;
|
|
}
|
|
|
|
.palette .cells { display: grid; grid-template-columns: repeat(2, 17px); gap: 2px; }
|
|
.palette .slot { display: contents; }
|
|
.palette .cell {
|
|
width: 17px; height: 17px;
|
|
padding: 0;
|
|
border: 1px solid rgba(0, 0, 0, .35);
|
|
border-radius: 2px;
|
|
cursor: pointer;
|
|
}
|
|
.palette .cell:hover { border-color: var(--fg); transform: scale(1.12); }
|
|
.palette .cell.on { border-color: var(--fg); box-shadow: 0 0 0 1px var(--pane), 0 0 0 2px var(--sel); position: relative; z-index: 1; }
|
|
|
|
/* Clear is no colour: a knockout. Checkered, as transparency is everywhere. */
|
|
.palette .clear,
|
|
.palette .chip.clear {
|
|
background-color: #fff;
|
|
background-image:
|
|
linear-gradient(45deg, #c9c9c5 25%, transparent 25%, transparent 75%, #c9c9c5 75%),
|
|
linear-gradient(45deg, #c9c9c5 25%, transparent 25%, transparent 75%, #c9c9c5 75%);
|
|
background-size: 8px 8px;
|
|
background-position: 0 0, 4px 4px;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
on the stage: the tools' marks, in stage pixels (the SVG's viewBox), and their
|
|
cursors. Hairlines, so the pixels under them can be judged. */
|
|
|
|
.paint-overlay.tool-pen {
|
|
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22'%3E%3Cpath d='M2 2 L9 5 L14 11 L11 14 L5 9 Z' fill='%23111' stroke='%23fff' stroke-width='1.2' stroke-linejoin='round'/%3E%3Cpath d='M2 2 L7 7' stroke='%23fff' stroke-width='1'/%3E%3C/svg%3E") 2 2, crosshair;
|
|
}
|
|
.paint-overlay.tool-pen.closing {
|
|
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22'%3E%3Cpath d='M2 2 L9 5 L14 11 L11 14 L5 9 Z' fill='%23111' stroke='%23fff' stroke-width='1.2' stroke-linejoin='round'/%3E%3Ccircle cx='17' cy='17' r='3' fill='none' stroke='%23111' stroke-width='1.6'/%3E%3Ccircle cx='17' cy='17' r='3' fill='none' stroke='%23fff' stroke-width='.6'/%3E%3C/svg%3E") 2 2, crosshair;
|
|
}
|
|
.paint-overlay.tool-brush, .paint-overlay.tool-eraser { cursor: none; }
|
|
|
|
.paint-overlay .footprint { fill: none; stroke: #fff1be; stroke-width: 0.5; vector-effect: non-scaling-stroke; pointer-events: none; }
|
|
.paint-overlay .footprint.eraser { stroke-dasharray: 3 2; }
|
|
|
|
.paint-overlay .draft polyline { fill: none; stroke: #fff1be; stroke-width: 1; vector-effect: non-scaling-stroke; pointer-events: none; }
|
|
.paint-overlay .draft .first { fill: #161820; stroke: #fff1be; stroke-width: 0.5; pointer-events: none; }
|
|
.paint-overlay .draft .first.closing { fill: #fff1be; r: 2.6; }
|
|
|
|
.paint-overlay .points .outline { fill: none; stroke: #fff1be; stroke-width: 1; stroke-opacity: .55; vector-effect: non-scaling-stroke; pointer-events: none; }
|
|
.paint-overlay .points .vertex { fill: #fff1be; stroke: #161820; stroke-width: 0.5; cursor: move; }
|
|
.paint-overlay .points .vertex:hover { fill: #fff; r: 2.4; }
|
|
.paint-overlay .points .insert { fill: #161820; stroke: #fff1be; stroke-width: 0.5; pointer-events: none; }
|
|
|
|
/* Blender's Adjust Last Operation: the last stroke's settings, in the corner of
|
|
the view it was made in, until something else is done. */
|
|
.adjust-last {
|
|
position: absolute;
|
|
left: 10px; bottom: 10px;
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 5px 9px;
|
|
background: var(--pane);
|
|
border: 1px solid var(--line);
|
|
border-radius: 4px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .25);
|
|
}
|
|
|
|
/* Remap: the map swatch, and the inspector's grid for a remap shape. A slot is
|
|
shown as what is under the shape will be drawn in; the original is the chip
|
|
in its corner. */
|
|
.palette .cell.map,
|
|
.palette .chip.map {
|
|
background: linear-gradient(135deg, #3a3f52 0 50%, #e8d9a8 50% 100%);
|
|
color: #fff; font-size: 11px; line-height: 1; text-shadow: 0 1px 1px rgba(0, 0, 0, .6);
|
|
}
|
|
.palette .cell { position: relative; }
|
|
.palette .cell .badge {
|
|
position: absolute; right: -2px; bottom: -2px;
|
|
width: 7px; height: 7px;
|
|
border: 1px solid var(--pane); border-radius: 50%;
|
|
}
|
|
|
|
.remap-head { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
|
.remap, .remap-pick { display: grid; grid-template-columns: repeat(8, 22px); gap: 3px; }
|
|
.remap-pick { margin-top: 7px; padding-top: 7px; border-top: 1px solid var(--hair); }
|
|
.remap-pick > .dim { grid-column: 1 / -1; }
|
|
.remap .cell, .remap-pick .cell {
|
|
position: relative;
|
|
width: 22px; height: 22px;
|
|
padding: 0;
|
|
border: 1px solid rgba(0, 0, 0, .35);
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
}
|
|
.remap .cell:hover, .remap-pick .cell:hover { border-color: var(--fg); }
|
|
.remap .cell.on, .remap-pick .cell.on { box-shadow: 0 0 0 1px var(--pane), 0 0 0 2px var(--sel); }
|
|
.remap .cell.mapped i {
|
|
position: absolute; left: 2px; top: 2px;
|
|
width: 8px; height: 8px;
|
|
border: 1px solid rgba(255, 255, 255, .8);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* The palette's own settings, at the foot of the strip: drawn as a tool button,
|
|
because it is one more thing in the strip you press. */
|
|
.palette-assets > button {
|
|
width: 34px; height: 30px; padding: 0;
|
|
display: grid; place-items: center;
|
|
border: 1px solid transparent; border-radius: 4px; background: none;
|
|
}
|
|
.palette-assets > button:hover:not(:disabled) { background: var(--sunk); border-color: var(--hair); }
|
|
.palette-assets > button.on { background: var(--sel-bg); border-color: var(--sel); }
|
|
.palette-assets svg path { fill: var(--fg); }
|
|
.palette-assets svg circle { fill: var(--chrome); }
|
|
.palette-assets > button.on svg circle { fill: var(--sel-bg); }
|
|
|
|
/* A popover's panel: settings, not commands, so narrower than a menu and laid
|
|
out as a form. Placed and dismissed by `ui/menu`. */
|
|
.menu.popout { width: 240px; min-width: 0; padding: 10px; line-height: 1.4; }
|
|
.menu.popout:has(.palette-popout) { width: 280px; }
|
|
.palette-popout { display: grid; gap: 8px; }
|
|
.palette-popout .row { margin: 0; }
|
|
|
|
/* Every palette as its strip and its name: what you would switch to, seen
|
|
before you switch. The open one is the selected row. */
|
|
.palette-list { display: grid; gap: 1px; max-height: 50vh; overflow: auto; }
|
|
.palette-row { display: flex; align-items: center; gap: 2px; border-radius: 3px; }
|
|
.palette-row.on { background: var(--sel-bg); }
|
|
.palette-row .choose {
|
|
flex: 1; min-width: 0;
|
|
display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 3px 8px;
|
|
padding: 5px 6px; border: 1px solid transparent; border-radius: 3px; background: none; text-align: left;
|
|
}
|
|
.palette-row button.choose:hover:not(:disabled) { background: var(--sunk); border-color: var(--hair); }
|
|
.palette-row.on button.choose { border-color: var(--sel); }
|
|
.palette-row.on button.choose:hover { background: var(--sel-bg); }
|
|
.palette-row .strip { grid-column: 1 / -1; }
|
|
.palette-row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.palette-row.on .name { font-weight: 600; color: #14395f; }
|
|
.palette-row .palette-name { grid-column: 1 / -1; width: 100%; min-width: 0; }
|
|
.palette-row .rename { align-self: stretch; padding: 0 7px; border-color: transparent; background: none; }
|
|
.palette-row .rename:hover:not(:disabled) { border-color: var(--sel); background: #fff; }
|
|
|
|
.strip { display: flex; height: 14px; border: 1px solid rgba(0, 0, 0, .35); border-radius: 2px; overflow: hidden; }
|
|
.strip i { flex: 1; }
|
|
|
|
/* On the stage at the playhead. */
|
|
.on-stage { width: 7px; height: 7px; border-radius: 50%; background: var(--live); }
|
|
|
|
/* A palette being edited that the stage is not drawing in here. Information,
|
|
not alarm: the accent of a selection, not of a warning. */
|
|
.palette-assets-wrap { position: relative; }
|
|
.palette-info {
|
|
position: absolute; top: -3px; right: -5px;
|
|
width: 13px; height: 13px; border-radius: 50%;
|
|
display: grid; place-items: center;
|
|
background: var(--sel); color: #fff;
|
|
font: italic 600 9px/1 Georgia, serif;
|
|
cursor: help;
|
|
box-shadow: 0 0 0 1.5px var(--chrome);
|
|
}
|
|
.info-card {
|
|
position: fixed; z-index: 60;
|
|
width: 240px; padding: 8px 10px;
|
|
display: grid; gap: 3px;
|
|
background: var(--pane); color: var(--fg);
|
|
font: 11px/1.45 "Lucida Grande", "Segoe UI", system-ui, sans-serif;
|
|
border: 1px solid var(--sel); border-left-width: 3px; border-radius: 3px;
|
|
box-shadow: 0 3px 10px rgba(0, 0, 0, .25);
|
|
cursor: default;
|
|
}
|
|
.info-card strong { color: #14395f; }
|
|
.view-popout label { display: block; margin-bottom: 8px; }
|
|
.view-popout label:last-child { margin-bottom: 0; }
|
|
.view-popout input, .view-popout select { width: 100%; }
|
|
|
|
/* An overlay switch and its settings: one object, two parts. */
|
|
.overlay > .menu-wrap > button { margin-left: -1px; padding: 1px 5px; border-radius: 0 2px 2px 0; }
|
|
.overlay > button { border-radius: 2px 0 0 2px; }
|
|
.stage-wrap { flex: 0 0 auto; }
|
|
|
|
|
|
.onion-skin { position: absolute; inset: 0; pointer-events: none; image-rendering: pixelated; }
|
|
|
|
|
|
/* Expanded automation lanes expose editable keyframe dots. */
|
|
button.tl-key { pointer-events: auto; padding: 0; border: 0; width: 11px; height: 11px; margin: -5.5px; cursor: ew-resize; z-index: 4; opacity: 1; border-radius: 50%; }
|
|
button.tl-key.selected { background: var(--pick); box-shadow: 0 0 0 2px var(--pick); }
|