The grid was five panes at three fixed widths and one fixed height, written as lengths in the stylesheet. That is a claim about every screen the tool is ever opened on, and it was wrong on two of them: a phone, where 210 + 250 pixels of side pane leave nothing for a 320x200 stage, and a large display, where the inspector is the width somebody once typed rather than the width their work wants. So the tracks are custom properties and `ui/layout` owns the numbers. A drag of an edge is one `assoc-in` and one property on `.app`; no pane's contents re-render, which is the same reason the picture is painted by `ui/player`'s loop rather than by anything reactive. Sizes stay in PIXELS and not fractions — three of these panes hold fixed-width rows, so what you drag an edge to is the width you meant at any window size. State, events and widgets are one namespace, which is not this repo's shape anywhere else. They are the same six integers: `:pool` is a width, a grid track, a drag's clamp and a button's label at once, and splitting six integers across `db`, `events/` and `ui/` is more wiring than state. Three things follow from making it state at all: SHUTTING A PANE. Pool and inspector unmount; the timeline collapses to its transport strip instead, because play, pause and the frame readout live in that strip and a window with nowhere to press play is broken rather than small. The toggles are in the top bar because a shut pane has no head left to carry its own handle. A NARROW WINDOW. Below 760px the grid is one column and the side panes are drawers over the stage rather than columns beside it. The breakpoint is read once, at load, and only decides what the layout OPENS at: throwing away the sizes somebody dragged because they turned a tablet sideways is worse than a layout that is briefly the wrong shape. ZOOM. The stage's was already a constant 2 in `ui/stage`; it is now an integer in [1 8], still CSS over a canvas that is the raster's own size, so the browser suite still reads 320x200 of real pixels off `canvas.stage`. Whole pixels only — a fractional scale under `image-rendering: pixelated` draws some rows of the raster thicker than others, which misrepresents the one thing the preview exists to judge. The timeline's is the stylesheet's entirely: every mark in the tracks is positioned as a percentage of their width, so one multiplier on that width spreads the grid, the spans, the keys, the ruler and the playhead apart together. Both readouts are the button back to normal, accented while there is something to return from, because the quick way back belongs in the one place you already look to find out where you are. One trap, paid for and then found: the top strip has to scroll sideways on a narrow screen, and `overflow-x: auto` makes an element a clipping container on BOTH axes. Every top-bar menu — open, snapshots, share, sign in — was then cut off by a 30px-tall bar and drawn at whatever x the strip happened to be scrolled to, which reads as the button doing nothing at all. They are fixed to the viewport there, as `.menu-drop` already was one pane down for the same reason. The same shape of bug ate the polygon tool and both zoom readouts: a flex row that does not fit shrinks every item and pushes the last ones off the end, so nothing in a strip shrinks now and the strip scrolls as a strip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1443 lines
49 KiB
CSS
1443 lines
49 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 */
|
|
|
|
/* 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); }
|
|
|
|
.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;
|
|
/* `safe`, because the stage zooms: a centred grid 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. */
|
|
place-items: safe 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;
|
|
/* 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 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); }
|
|
.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; }
|
|
.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); }
|
|
|
|
/* 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; }
|
|
|
|
/* 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, .palette-bar { gap: 7px; }
|
|
.palette-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 > *, .palette-bar > * { flex-shrink: 0; }
|
|
.top button, .pane-head button { white-space: nowrap; }
|
|
.pane-head, .palette-bar { overflow-x: auto; }
|
|
|
|
/* The one compressible thing in the palette bar is the swatches, so they
|
|
scroll inside their own share instead of taking it from the commands. The
|
|
tone's NAME goes: the ringed swatch already says which one is active, and
|
|
it is the widest thing in the bar that nothing is lost by dropping. */
|
|
.palette-bar .swatches { flex: 1 1 130px; min-width: 56px; overflow-x: auto; }
|
|
/* A swatch is a 15px circle or it is not a swatch: the strip scrolls, the
|
|
dots do not get thinner. */
|
|
.palette-bar .swatches > * { flex-shrink: 0; }
|
|
.palette-bar > .dim { display: none; }
|
|
}
|