arthur/static/arthur/app.css
2026-10-01 01:21:55 -04:00

1256 lines
40 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* The application chrome: one screen, five panes, no scrolling page.
*
* Macromedia-era, deliberately. Light grey panels, hairline rules, small type,
* and a timeline whose keyframes are DOTS on a frame grid — Flash's vocabulary,
* because this tool's model is Flash's model (a library of timelines, instances
* placed on a stage, a layer list against a ruler) and borrowing the look is the
* cheapest way to make that legible to anyone who has used one. What is not
* borrowed is the chrome of the period: no bevels, no gradients on buttons, no
* inset wells. Flat, clean, one accent.
*
* Raw CSS on purpose. This file styles a fixed grid of panes whose class names
* are the domain's own — `.tl-row`, `.pool-item`, `.swatch` — so a utility
* framework would buy naming we want anyway, at the cost of a third process in
* a dev loop the repo keeps at two. Tokens are custom properties; everything
* else is a handful of grids.
*
* It lives under static/ rather than inline in the template because it is now
* long enough that "the styles" and "the page" are two things. */
:root {
color-scheme: light;
/* The stage's background is the PALETTE's index 0, not a theme colour. The
two agreeing is what makes the preview honest, so it is written here as the
same value arthur.domain.palette declares and nowhere else in this file.
It is the one dark surface on the page, and it should be: the picture is
320x200 of indexed colour and the UI is not allowed to compete with it. */
--stage: #12141c;
--desk: #8e8e8e; /* the work area the stage floats on */
--pane: #f2f2f0;
--chrome: #e4e4e1;
--sunk: #ebebe8;
--line: #b0b0ac;
--hair: #d2d2ce;
--fg: #1f1f1f;
--dim: #6e6e6a;
/* One accent, used for selection and for nothing else. */
--sel: #2f6fc0;
--sel-bg: #cfe0f5;
/* A keyframe is a dot and a dot is ink. Flash draws them black and so does
this; the frames a node exists over are a pale tint behind them. */
--key: #1f1f1f;
--span: #dfe8f4;
--span-line:#a9bdd8;
--grid: #dcdcd8;
--grid-5: #c6c6c2;
--playhead: #c8322b;
--warn: #b4502a;
--row: 21px;
--ruler: 19px;
--label: 210px;
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; overflow: hidden; }
body {
background: var(--desk);
color: var(--fg);
font: 11px/1.45 "Lucida Grande", "Segoe UI", system-ui, sans-serif;
}
#app { height: 100%; }
/* The preview is nearest-neighbour everywhere. A browser that smoothed the
upscale would misrepresent the exact look the tool exists to judge, so this
rule is load-bearing rather than cosmetic. */
canvas { image-rendering: pixelated; }
audio { display: none; }
/* --------------------------------------------------------------------------
the frame */
.app {
display: grid;
height: 100%;
grid-template-columns: var(--label) minmax(0, 1fr) 250px;
/* The location bar is its own row and takes its height from the stage, not
from the timeline: it exists to explain what the timeline is showing, so
paying for it in timeline rows would be the wrong trade. */
grid-template-rows: 30px minmax(0, 1fr) 21px 232px;
grid-template-areas:
"top top top"
"pool view params"
"loc loc loc"
"time time time";
gap: 1px;
background: var(--line);
}
.top { grid-area: top; }
.pool { grid-area: pool; }
.view { grid-area: view; }
.params { grid-area: params; }
.loc { grid-area: loc; }
.time { grid-area: time; }
/* Every pane is its own scroll container. `min-height: 0` is what lets a grid
row shrink below its content instead of pushing the layout taller than the
viewport — the single line that separates "a page that scrolls" from "an
application window". */
.pane {
background: var(--pane);
min-height: 0;
min-width: 0;
overflow: auto;
display: flex;
flex-direction: column;
}
.pane-head {
position: sticky;
top: 0;
z-index: 2;
display: flex;
align-items: center;
gap: 5px;
padding: 0 7px;
height: 21px;
flex: 0 0 21px;
background: var(--chrome);
border-bottom: 1px solid var(--line);
color: var(--dim);
letter-spacing: .03em;
/* One line, always. A strip of controls that reflows to two lines resizes the
pane under it, and the pane under this one is the timeline. */
white-space: nowrap;
}
.pane-head .readout { color: var(--fg); }
/* A pick-one that looks like the other pick-ones. A native select in this strip
draws a tall white well and a chevron of the browser's choosing, which reads
as a form field among buttons; the app's own idiom for "choose one of these"
is `label ▾`, and this IS one of those. So: the select keeps the behaviour and
the keyboard, and borrows the look. The arrow is the wrapper's, because an
option list cannot carry one. */
.pick { position: relative; display: inline-flex; align-items: center; }
.pick > select {
appearance: none;
background: var(--pane);
padding: 1px 15px 1px 6px;
cursor: pointer;
}
.pick > select:hover { background: #fff; }
.pick::after {
content: "▾";
position: absolute;
right: 5px;
color: var(--dim);
pointer-events: none;
}
.pane-head .spacer { flex: 1; }
.pane-body { padding: 5px; }
/* --------------------------------------------------------------------------
controls */
button {
font: inherit;
color: var(--fg);
background: var(--pane);
border: 1px solid var(--line);
border-radius: 2px;
padding: 1px 7px;
cursor: pointer;
}
button:hover:not(:disabled) { background: #fff; }
button:active:not(:disabled) { background: var(--sunk); }
button:disabled { opacity: .38; cursor: default; }
button.on {
background: var(--sel-bg);
border-color: var(--sel);
color: #14395f;
}
select, input[type="text"] {
font: inherit;
color: var(--fg);
background: #fff;
border: 1px solid var(--line);
border-radius: 2px;
padding: 1px 3px;
max-width: 100%;
}
input[type="range"] { width: 100%; accent-color: var(--sel); }
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
}
.dim { color: var(--dim); }
.warn { color: var(--warn); }
/* Buttons that are one control: a transport, a stepper, a mode picker. They
share their borders, so the group reads as a single object with parts rather
than as several things that happen to be adjacent — which is the whole claim
a segmented control makes, and the reason `timeline` and `cel sheet` are one
of these. `.seg` is `.group` with that meaning; they are drawn the same
because the difference is what the buttons do, not how they look.
The negative margin collapses the doubled border between two buttons into
one hairline. A pressed button then has to be raised, or the neighbour's
plain border covers half of its accent one. */
.group, .seg { display: flex; align-items: center; }
.group > button, .seg > button { border-radius: 0; margin-left: -1px; }
.group > button:first-child, .seg > button:first-child { margin-left: 0; border-radius: 2px 0 0 2px; }
.group > button:last-child, .seg > button:last-child { border-radius: 0 2px 2px 0; }
.group > button.on, .seg > button.on { position: relative; z-index: 1; }
/* The rule between two groups of unrelated controls. Shorter than the strip, so
it reads as a division of the row rather than as a wall across it. */
.sep { flex: 0 0 1px; width: 1px; height: 13px; background: var(--line); }
/* An icon button. The glyph is inked in `currentColor`, so `.on` colours it
with the same rule that colours a word. 11px to sit on 11px type. */
button.ico { padding: 2px 5px; line-height: 0; }
button.ico > svg { display: block; width: 11px; height: 11px; }
/* Play is the one control in the strip you aim at without looking. */
button.ico-play { padding-left: 8px; padding-right: 8px; }
/* `hold −` / `hold +`: one label over two steppers, because the word is shared
and repeating it in both buttons was most of their width. */
.stepper { display: flex; align-items: center; gap: 4px; }
.stepper-label { color: var(--dim); }
.stepper button { padding: 1px 6px; }
/* A number that changes every frame. Tabular figures stop it twitching, and
stop the controls after it being nudged about as the count passes 9 and 99. */
.readout { font-variant-numeric: tabular-nums; }
/* The answer to a refusal rather than a standing command — `lane-retry` only
exists because something was declined and named this as the way through. */
button.retry { border-color: var(--sel); color: var(--sel); }
/* --------------------------------------------------------------------------
top bar */
.top {
display: flex;
align-items: center;
gap: 7px;
padding: 0 8px;
background: var(--chrome);
}
.top .brand { color: var(--dim); letter-spacing: .14em; text-transform: uppercase; }
.top .status {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--dim);
}
.project-title { max-width: 22vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.project-name { width: 180px; }
.export-scrim { position: fixed; inset: 0; z-index: 50; background: rgba(0, 0, 0, .45); }
.export-dialog {
position: fixed; z-index: 51; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: min(360px, calc(100vw - 32px)); padding: 14px;
background: var(--pane); border: 1px solid var(--line); border-radius: 3px;
box-shadow: 0 8px 30px rgba(0, 0, 0, .45);
}
.export-dialog h2 { margin: 0 0 12px; font: inherit; font-weight: 600; }
.export-field { display: grid; gap: 4px; margin: 8px 0; color: var(--dim); }
.export-field select { color: var(--fg); }
/* --------------------------------------------------------------------------
the open menu */
.menu-wrap { position: relative; }
/* A full-page catcher behind the panel, so a click anywhere else dismisses it.
More predictable than a document listener that has to be added, removed, and
told to ignore the click that opened the menu. */
.menu-scrim { position: fixed; inset: 0; z-index: 40; }
.menu {
position: absolute;
top: calc(100% + 3px);
right: 0;
z-index: 41;
min-width: 250px;
max-height: 72vh;
overflow: auto;
padding: 5px;
background: var(--pane);
border: 1px solid var(--line);
border-radius: 2px;
box-shadow: 0 3px 10px rgba(0, 0, 0, .3);
}
.menu > h2 {
margin: 6px 0 3px;
font: inherit;
font-weight: 600;
color: var(--dim);
text-transform: uppercase;
letter-spacing: .07em;
}
.menu > h2:first-child { margin-top: 0; }
.menu > h2 + .dim { margin-bottom: 4px; }
.menu-item {
display: block;
width: 100%;
text-align: left;
padding: 2px 6px;
border: 1px solid transparent;
border-radius: 2px;
background: none;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.menu-item:hover:not(:disabled) { background: var(--sel-bg); }
/* A fixture is not a document. Italic is the whole of the distinction, and it is
enough: the heading above already says what these are. */
.menu-item.example { font-style: italic; }
.menu-item .sub { display: block; color: var(--dim); }
/* Rows about the document rather than choices in it: who can edit. */
.menu-item.static { display: flex; align-items: center; gap: 6px; cursor: default; }
.menu-item.static:hover { background: none; }
.menu-item.static .sub { display: inline; margin-left: auto; }
.menu-item.static button { margin-left: auto; }
.menu .row { margin: 4px 0; flex-wrap: nowrap; }
.menu .row input { flex: 1; min-width: 0; }
.menu p { margin: 4px 0; }
.account { display: grid; gap: 4px; }
button.link { border: 0; background: none; padding: 0; color: var(--sel); cursor: pointer; }
/* `/`: your projects, over the editor. */
.index { position: fixed; inset: 0; z-index: 30; display: flex; flex-direction: column;
background: var(--pane); }
.index-body { width: min(640px, 100%); margin: 0 auto; padding: 24px 16px; overflow: auto; }
.index-body h1 { font: inherit; font-size: 18px; font-weight: 600; margin: 0 0 12px; }
.index-head { display: flex; align-items: baseline; justify-content: space-between; }
.index-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--hair); }
.index-list li { display: flex; justify-content: space-between; gap: 12px;
padding: 7px 2px; border-bottom: 1px solid var(--hair); }
.index-list a { color: var(--fg); text-decoration: none; font-weight: 600; }
.index-list a:hover { color: var(--sel); }
a.brand { text-decoration: none; }
/* Undo, its list, redo: one group. */
.undo { display: flex; gap: 0; }
.undo > button { border-radius: 0; }
.undo > button:first-child { border-radius: 2px 0 0 2px; }
.undo > .undo-list { padding: 0 4px; margin-right: 4px; border-left: 0; border-radius: 0 2px 2px 0; }
.menu.menu-left { left: 0; right: auto; }
/* A menu dropping out of a PANE HEAD. Fixed and placed by `ui/menu` against the
button, because a pane clips its own overflow and an absolutely positioned
panel would be cut off at the pane's edge — invisibly, and more of it the
longer the menu gets. `top`/`bottom`/`left`/`max-height` all come from there.
Wide enough that a command's one-line explanation is one line. At 215px every
`.sub` in the cel menu wrapped, which doubled the panel's height for no gain:
the explanations are the reason these are menu rows and not buttons, so they
get the width to be read in. */
.menu.menu-drop {
position: fixed;
/* BOTH offsets on each axis are reset, and `ui/menu` sets exactly one of each
inline. Leaving `.menu`'s own `top` standing alongside an inline `bottom`
over-constrains the box, and a fixed box given both offsets takes its HEIGHT
from them — which, from a toolbar near the foot of the window, computes
negative and collapses the panel to its padding with the commands still
inside it. */
top: auto;
bottom: auto;
left: auto;
right: auto;
width: 272px;
overflow: auto;
}
.menu-note { padding: 1px 6px 5px; }
/* A disabled row still says what the command is FOR. That is the whole trade:
a greyed button hides its reason in a `title`, a greyed row prints it. */
.menu-item:disabled { opacity: .5; cursor: default; }
.menu-item:disabled:hover { background: none; }
.menu-item { padding: 3px 6px; }
.menu-item .sub { font-size: 10px; line-height: 1.3; white-space: normal; }
/* The one way to hand the document to somebody else, so it looks like one. */
button.share-button { background: var(--sel); color: #fff; border-color: var(--sel); font-weight: 600; padding: 1px 12px; }
button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
/* Who else has this document open. */
.roster { display: flex; gap: 2px; align-items: center; }
.peer {
width: 18px; height: 18px; border-radius: 50%;
display: inline-grid; place-items: center;
background: var(--sel); color: #fff; font-size: 10px; font-weight: 600;
}
.peer.guest { background: var(--dim); }
/* --------------------------------------------------------------------------
media pool
A FILE LIST, AND IT IS 210px WIDE. Every row is one line — a picture at the
stage's own proportions, a name, one number — because the pane under this one
is the timeline and a row that costs two lines costs timeline rows. The row is
--row tall, which is a timeline row, so a pool of symbols and the rows they
end up on are the same size object.
One exception, deliberately: the main timeline. See `.pool-row.main`. */
/* The search strip, between the pane head and the tabs. Its own band rather
than crowded into the head, which is one line and already holds the + . */
.pool-find {
position: sticky;
top: 21px;
z-index: 2;
display: flex;
align-items: center;
gap: 4px;
padding: 3px 5px;
background: var(--chrome);
border-bottom: 1px solid var(--hair);
}
.pool-search {
flex: 1;
min-width: 0;
height: 17px;
padding: 0 5px;
font: inherit;
color: var(--fg);
background: var(--pane);
border: 1px solid var(--line);
border-radius: 2px;
}
/* Safari draws its own clear button inside a search input, in its own idiom and
at its own size. The one next to it is this file's. */
.pool-search::-webkit-search-decoration,
.pool-search::-webkit-search-cancel-button { -webkit-appearance: none; }
.pool-clear {
flex: 0 0 auto;
width: 17px;
height: 17px;
padding: 0;
border: 0;
border-radius: 2px;
background: none;
color: var(--dim);
cursor: pointer;
}
.pool-clear:hover { background: var(--hair); color: var(--fg); }
/* The scope tabs, in the vocabulary `ui/tabs` already established for symbols —
the same object, so two rows of tabs on one screen read as one idea. */
.pool > .tabs { top: 45px; position: sticky; z-index: 2; }
/* A tally, shown only on the tab you are NOT looking at and only while a search
is running: it says "what you asked for is over here". */
.tab .count {
padding: 0 4px;
border-radius: 7px;
background: var(--sel-bg);
color: var(--sel);
font-variant-numeric: tabular-nums;
}
/* MEDIA, SOUNDS, TIMELINES. A heading is one line and it sticks, so scrolling a
long list never leaves you looking at rows with no idea what kind they are. */
.pool-section { margin-bottom: 7px; }
.pool-section > h2 {
position: sticky;
top: 69px;
z-index: 1;
margin: 0 0 2px;
padding: 1px 5px;
font: inherit;
font-weight: 600;
color: var(--dim);
text-transform: uppercase;
letter-spacing: .07em;
background: var(--pane);
}
.pool-section > .dim { padding: 0 5px 2px; }
.pool-rows { display: flex; flex-direction: column; }
/* What a search found nothing to say. */
.pool-empty { padding: 6px 5px; color: var(--dim); }
.pool-project { margin: 0 0 1px 5px; }
.pool-project > summary { cursor: pointer; color: var(--dim); padding: 1px 0; }
.pool-project > .pool-rows { margin-left: 8px; }
/* THE ROW IS THE BUTTON. Click selects, double-click opens, and drag starts
anywhere across it — the whole strip, not the few characters of the name. The
wrapper carries the state classes and the frame; the button inside it fills
that frame edge to edge, so there is no dead strip anywhere along the row. */
.pool-row {
display: flex;
align-items: center;
gap: 2px;
min-width: 0;
height: var(--row);
padding: 0 4px;
border: 1px solid transparent;
border-radius: 2px;
}
.pool-row:hover:not(.disabled) { background: #fff; }
.pool-row.on { background: var(--sel-bg); border-color: var(--sel); }
/* WHICH ONE IS OPEN, as a mark and not as the word "open" — a list this narrow
cannot spend twenty pixels of every name on a word that is true of one row.
The accent, because selection is the only thing it is ever used for and the
open symbol is what the rest of the screen is showing. */
.pool-row.open { box-shadow: inset 2px 0 0 var(--sel); }
.pool-item {
display: flex;
align-items: center;
gap: 5px;
flex: 1;
min-width: 0;
padding: 0;
text-align: left;
border: 0;
background: none;
color: inherit;
font: inherit;
cursor: grab;
}
/* Shrinks to the name, so the pencil sits against the end of the word rather
than out at the edge of the pane — and ellipses rather than pushing the
number off the row. */
.pool-item .text { display: flex; flex-direction: column; flex: 0 1 auto; min-width: 0; }
.pool-item .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The number, at the far edge and never moving: `auto` on the left is what
pushes it there, and tabular figures are what stop it shuffling as the rows
scroll past. */
.pool-item > .sub {
flex: 0 0 auto;
margin-left: auto;
padding-left: 4px;
color: var(--dim);
font-variant-numeric: tabular-nums;
}
/* Every row leads with a picture, and they are all this box, so the names line
up down the list whatever is beside them. 16:10 is the stage's own ratio at
320x200 — a thumbnail that is not the picture's shape is a thumbnail you have
to think about. */
.pool-item .thumb {
flex: 0 0 32px;
width: 32px;
height: 20px;
object-fit: cover;
background: var(--stage);
border-radius: 1px;
pointer-events: none;
}
.pool-item .thumb.sound {
display: flex;
align-items: center;
justify-content: center;
color: #d9d9d9;
}
/* THE MAIN TIMELINE, under the PROJECT heading and at the top of the pane.
`clip/opens-on`'s answer: the longest symbol nothing else places, which is
where the work happens. The one row in the pane given size, a second line and
a surface of its own — spent here rather than spread over every row, which is
what keeps the list below it a list. */
.pool-row.main {
height: auto;
margin-bottom: 5px;
padding: 4px;
background: var(--sunk);
border-color: var(--hair);
}
.pool-row.main .thumb {
flex: 0 0 64px;
width: 64px;
height: 40px;
}
.pool-row.main .name { font-weight: 600; }
/* Both facts on one line under the name. Beside a 64px picture there is room,
and this is the one row where a person wants more than the length before
they click. */
.pool-row.main .detail {
color: var(--dim);
font-variant-numeric: tabular-nums;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The pencil. A span and not a button — see `ui/pool`'s `row` — so it needs its
own centring and cursor rather than inheriting a control's.
Hidden until the row is under the pointer or holds focus, because a column of
twenty pencils is a column of twenty pencils; `visibility` rather than
`display`, so it keeps its width and the name beside it does not reflow as the
pointer crosses the list. */
.pool-rename {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 15px;
width: 15px;
height: 15px;
border-radius: 2px;
color: var(--dim);
line-height: 1;
cursor: pointer;
visibility: hidden;
}
.pool-row:hover .pool-rename,
.pool-row:focus-within .pool-rename { visibility: visible; }
.pool-rename:hover { background: var(--hair); color: var(--fg); }
/* Renaming. The input REPLACES the row at the same height, so a list does not
jump while you type in it. */
.pool-name {
flex: 1;
min-width: 0;
height: 17px;
padding: 0 4px;
font: inherit;
color: var(--fg);
background: #fff;
border: 1px solid var(--sel);
border-radius: 2px;
}
/* The whole pane is the drop target, so the cue has to be the pane and not a
zone inside it — a drop that lands two pixels outside a dashed rectangle and
silently does nothing is the failure this avoids. */
.pool.dropping {
outline: 2px dashed var(--sel);
outline-offset: -3px;
background: var(--sel-bg);
}
/* --------------------------------------------------------------------------
the viewer: palette above, stage below */
.view {
display: flex;
flex-direction: column;
background: var(--desk);
min-height: 0;
overflow: hidden;
}
/* One tab per open symbol: flat, on the chrome, with the open one marked by the
selection colour underneath rather than by a raised shape. */
.tabs {
display: flex;
align-items: stretch;
height: 24px;
flex: 0 0 24px;
padding: 0 4px;
background: var(--chrome);
border-bottom: 1px solid var(--line);
overflow-x: auto;
}
.tab {
display: flex;
align-items: center;
gap: 4px;
padding: 0 6px 0 10px;
color: var(--dim);
border-right: 1px solid var(--hair);
cursor: pointer;
white-space: nowrap;
}
.tab:hover { color: var(--fg); background: var(--sunk); }
.tab.on { color: var(--fg); background: var(--pane); box-shadow: inset 0 -2px 0 var(--sel); }
.tab .close {
width: 16px;
height: 16px;
padding: 0;
border: 0;
border-radius: 2px;
background: none;
color: var(--dim);
line-height: 16px;
visibility: hidden;
}
.tab:hover .close, .tab.on .close { visibility: visible; }
.tab .close:hover { background: var(--hair); color: var(--fg); }
.trace-opacity { width: 64px; }
.palette-bar label.dim { display: inline-flex; align-items: center; gap: 2px; white-space: nowrap; }
.palette-bar {
display: flex;
align-items: center;
gap: 9px;
padding: 4px 8px;
background: var(--chrome);
border-bottom: 1px solid var(--line);
}
.swatches { display: flex; gap: 3px; }
/* Circles. A palette entry is one indivisible tone, not an area of coverage, and
a row of dots says that where a row of tiles says "swatch book". */
.swatch {
width: 15px;
height: 15px;
padding: 0;
border: 1px solid var(--line);
border-radius: 50%;
cursor: pointer;
}
.swatch:hover:not(:disabled) { border-color: var(--fg); }
/* A swatch that cannot be picked still has to SHOW ITS COLOUR: the generic
disabled rule fades it towards the pane, which turned the background slot —
the darkest tone in the palette — into pale grey, i.e. into a lie about what
index 0 is. Slot 0 says what it is with a ring instead. */
.swatch:disabled { opacity: 1; }
.swatch.bg { cursor: default; box-shadow: inset 0 0 0 2px var(--pane); }
.swatch.on {
border-color: var(--fg);
box-shadow: 0 0 0 2px var(--pane), 0 0 0 3px var(--sel);
}
/* An empty slot has to read as "no colour here" rather than as a pale colour,
which a flat fill at this size cannot do. */
.swatch.empty {
cursor: default;
background: #fff;
border-style: dashed;
border-color: var(--hair);
}
.stage-area {
flex: 1;
min-height: 0;
display: grid;
place-items: center;
overflow: auto;
padding: 14px;
}
/* The stage sits ON the work area rather than filling it, as it does in every
tool of this shape: the picture has edges and they are part of what you are
judging. */
.stage-wrap {
position: relative;
line-height: 0;
box-shadow: 0 1px 5px rgba(0, 0, 0, .35);
outline: 1px solid rgba(0, 0, 0, .45);
}
.stage { display: block; background: var(--stage); }
.paint-overlay { position: absolute; inset: 0; touch-action: none; }
/* The footage traced over: a reference above the picture, never part of it. */
.underlay { position: absolute; inset: 0; pointer-events: none; }
.paint-overlay.drawing { cursor: crosshair; }
.paint-overlay circle { cursor: grab; }
/* Where a drag out of the pool would land. Dashed and unfilled, so it reads as
"not there yet" over whatever is already drawn. */
/* Never a hit target: it is drawn under the pointer, and anything under the
pointer that changes mid-drag gets a dragenter of its own. */
.paint-overlay .ghost { pointer-events: none; }
.paint-overlay .ghost > * {
fill: none;
stroke: #fff1be;
stroke-width: 1;
stroke-dasharray: 3 2;
vector-effect: non-scaling-stroke;
}
/* --------------------------------------------------------------------------
params */
.section { border-bottom: 1px solid var(--hair); padding: 6px; }
.section:last-child { border-bottom: 0; }
.section > h2 {
margin: 0 0 5px;
font: inherit;
font-weight: 600;
color: var(--dim);
text-transform: uppercase;
letter-spacing: .07em;
}
.inspector-form { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; margin-top: 7px; }
.inspector-field { display: grid; gap: 2px; color: var(--dim); }
.inspector-field input { width: 100%; color: var(--fg); }
.correction-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
.correction-grid select, .correction-grid input { width: 100%; color: var(--fg); }
.correction-list, .correction-conflicts { display: grid; gap: 4px; margin-top: 7px; }
.correction-item { display: flex; align-items: center; gap: 4px; min-width: 0; }
.correction-item > span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Label / value, once, for every read-only fact in the pane. */
.facts { display: grid; grid-template-columns: auto 1fr; gap: 2px 8px; margin: 0; }
.facts dt { color: var(--dim); }
.facts dd { margin: 0; overflow: hidden; text-overflow: ellipsis; }
.knob { display: grid; gap: 1px; margin-bottom: 6px; }
.knob .top-line { display: flex; justify-content: space-between; gap: 8px; }
.knob .top-line .name { color: var(--dim); }
.regeneration-debug {
padding: 4px 5px;
background: var(--sunk);
border: 1px solid var(--hair);
border-radius: 2px;
}
/* --------------------------------------------------------------------------
the location bar
Where you are, above the timeline that draws it. Chrome-coloured like a pane
head, because that is what it is to the two temporal views below it — but it
is not one of their heads, since it says the same thing whichever is showing. */
.loc {
display: flex;
align-items: center;
gap: 7px;
padding: 0 7px;
min-width: 0;
background: var(--chrome);
color: var(--dim);
white-space: nowrap;
overflow: hidden;
}
.crumbs { display: flex; align-items: center; gap: 1px; min-width: 0; overflow: hidden; }
/* A crumb is a place, not a command: no border and no fill until it is pointed
at. The trail has to read as one sentence, and five outlined buttons in a row
read as five things to press. */
.crumb {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
padding: 1px 4px;
border: 1px solid transparent;
border-radius: 2px;
background: none;
color: var(--dim);
}
.crumb:hover:not(:disabled) { background: var(--sel-bg); color: var(--fg); }
/* The last crumb is the selection itself. Weight and full contrast say so; the
fill and accent border `button.on` would otherwise give it are undone here,
because a filled crumb reads as a pressed control and the thing it marks is
where you ARE, not something switched on. */
.crumb.on { color: var(--fg); font-weight: 600; background: none; border-color: transparent; }
.crumb.lane::before { content: "≡ "; color: var(--dim); font-weight: 400; }
.crumb-sep { flex: 0 0 auto; color: var(--line); }
.loc-fact { flex: 0 0 auto; }
.loc-fact::before, .loc-shared::before { content: "· "; color: var(--line); }
.loc-shared { flex: 0 0 auto; display: inline-flex; align-items: baseline; gap: 5px; }
.loc .spacer { flex: 1; }
/* --------------------------------------------------------------------------
timeline */
.time { overflow: hidden; }
.tl-body {
flex: 1;
min-height: 0;
display: flex;
/* Not `stretch`: that sizes both columns to the body's height, the rows then
overflow them, and the sticky ruler goes out with its column. Each column is
as tall as its rows instead, and at least as tall as the body. */
align-items: flex-start;
overflow: auto;
}
.tl-labels,
.tl-tracks { min-height: 100%; }
.tl-labels {
flex: 0 0 var(--label);
/* A flex item's `min-width` defaults to `auto`, which floors its used size at
its MIN-CONTENT width — so a row labelled with a uuid widens the whole
column and pushes the tracks out of alignment with the ruler above them.
`0` is what makes the declared basis the actual width. */
min-width: 0;
position: sticky;
left: 0;
/* Over the ruler and the playhead, which both pass under it when the tracks
scroll sideways. */
z-index: 4;
background: var(--pane);
border-right: 1px solid var(--line);
}
.tl-tracks {
flex: 1;
min-width: 340px;
position: relative;
background: #fff;
/* The frame grid, five frames to a division, as a background rather than as
an element per frame: a 900-frame take is 900 divs nobody needs in the DOM.
`--tick` is five frames as a percentage of the span, set from the component
because only it knows how long the clip is. */
background-image:
repeating-linear-gradient(90deg,
var(--grid-5) 0 1px, transparent 1px var(--tick, 10%));
}
.tl-label,
.tl-track {
height: var(--row);
border-bottom: 1px solid var(--hair);
}
/* The row is the containing block for its own span and keys. Without this they
resolve against `.tl-tracks` instead and every row's marks land stacked on the
first one, full height — which reads as "the timeline draws nothing" rather
than as a positioning bug. */
.tl-track { position: relative; }
.tl-label {
/* Scrolled to from the stage: clear of the sticky ruler above. */
scroll-margin-top: var(--ruler);
display: flex;
align-items: center;
gap: 3px;
padding-right: 6px;
cursor: default;
white-space: nowrap;
overflow: hidden;
}
.tl-label.on { background: var(--sel-bg); }
.tl-label:hover:not(.on) { background: #fff; }
.tl-label .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.tl-label .kind { color: var(--dim); }
/* A fixed-width cell whether or not there is a triangle in it, so names at the
same depth line up down the column. */
.tl-twist {
flex: 0 0 13px;
padding: 0;
border: 0;
border-radius: 0;
background: none;
color: var(--dim);
cursor: pointer;
text-align: center;
}
.tl-twist:hover:not(:disabled) { background: none; color: var(--fg); }
.tl-twist:disabled { opacity: 0; cursor: default; }
.tl-delete { margin-left: auto; padding: 0 4px; border: 0; background: none; color: var(--dim); }
.tl-delete:hover { color: var(--fg); }
.tl-solo, .tl-trace { margin-left: auto; padding: 0 4px; border: 0; background: none; color: var(--dim); font-size: 10px; }
.tl-solo:hover, .tl-trace:hover { color: var(--fg); }
.tl-solo.on, .tl-trace.on { color: var(--sel); font-weight: 600; }
.tl-solo + .tl-delete, .tl-trace + .tl-solo { margin-left: 0; }
/* The ruler and the corner above the labels stay at the top of the body while
the rows scroll under them: the ruler is the scrubber, and scrolling down to
a row is no reason to lose it. */
.tl-corner,
.tl-ruler {
position: sticky;
top: 0;
z-index: 2;
height: var(--ruler);
border-bottom: 1px solid var(--line);
background: var(--chrome);
}
.tl-ruler {
cursor: pointer;
user-select: none;
}
.tl-ruler .tick {
position: absolute;
top: 0;
bottom: 0;
border-left: 1px solid var(--line);
padding-left: 3px;
color: var(--dim);
font-size: 10px;
line-height: var(--ruler);
}
/* The heading between the picture's rows and the sounds', as an editor puts
its audio tracks under its video tracks. */
.tl-label.tl-section,
.tl-track.tl-section { background: var(--chrome); border-top: 1px solid var(--line); }
.tl-label.tl-section { padding-left: 6px; color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; }
/* A sound's bar, told apart from the picture's at a glance. */
.tl-span.sound { background: #e3efdf; border-color: #a6c49b; }
.pool-item .thumb.sound { display: flex; align-items: center; justify-content: center; color: #d9d9d9; }
/* The span the node exists over. Drawn under the keys so a dot on the first
frame of a span is not half-hidden by its own bar. */
.tl-span {
position: absolute;
top: 3px;
bottom: 3px;
background: var(--span);
border: 1px solid var(--span-line);
border-radius: 1px;
}
.tl-span.dense {
/* Generated, one value per frame: hatched, because ticking every frame would
be a solid block that says less than the bar behind it does. */
background: repeating-linear-gradient(
-45deg, var(--span) 0 3px, #c3d4ea 3px 6px);
}
/* The preview row of a drop in flight: its own length, where it would start. */
.tl-span.ghost, .tl-label.ghost { pointer-events: none; }
/* A row being dragged over another: into an instance, or grouped with a node,
by its middle; in front of it or behind it, by its top or bottom edge. */
.tl-label.drop-into { box-shadow: inset 0 0 0 2px var(--sel); background: var(--sel-bg); }
.tl-label.drop-group { outline: 2px dashed var(--sel); outline-offset: -2px; }
.tl-label.drop-front { box-shadow: inset 0 2px 0 var(--sel); }
.tl-label.drop-back { box-shadow: inset 0 -2px 0 var(--sel); }
/* A node's bar slides it along its symbol's time. */
.tl-span.movable { cursor: ew-resize; touch-action: none; }
.tl-span.movable.sliding { border-color: var(--sel); }
.tl-span.ghost {
background: transparent;
border: 1px dashed var(--sel);
}
.tl-label.ghost { color: var(--sel); font-style: italic; }
/* Flash's keyframe: a filled dot. */
.tl-key {
position: absolute;
top: 50%;
width: 7px;
height: 7px;
margin: -3.5px 0 0 -3.5px;
background: var(--key);
border-radius: 50%;
/* So grabbing a dot grabs the bar under it. */
pointer-events: none;
}
.tl-playhead {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: var(--playhead);
pointer-events: none;
z-index: 3;
}
/* The playhead's head, in the ruler rather than on the line, so it stays at the
top with the ruler while the rows scroll. */
.tl-knob {
position: absolute;
pointer-events: none;
margin-left: -4px;
top: 1px;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--playhead);
}
.tl-empty { padding: 9px; color: var(--dim); }
/* The second temporal view. Its cells carry the same selection addresses as
the cel blocks above; only the axes change. */
.cel-sheet {
flex: 1;
min-height: 0;
overflow: auto;
display: grid;
align-content: start;
background: var(--line);
gap: 1px;
}
.cs-head,
.cs-frame,
.cs-cell {
min-width: 0;
height: 24px;
border: 0;
border-radius: 0;
padding: 0 6px;
background: #fff;
color: var(--fg);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cs-head {
position: sticky;
top: 0;
z-index: 2;
display: flex;
align-items: center;
background: var(--chrome);
font-weight: 600;
cursor: pointer;
}
.cs-head.cs-frame { z-index: 3; }
.cs-frame { position: sticky; left: 0; z-index: 1; color: var(--dim); text-align: right; }
.cs-frame.on, .cs-cell.current { box-shadow: inset 3px 0 0 var(--playhead); }
.cs-cell { text-align: left; cursor: pointer; }
.cs-cell:hover { background: var(--sel-bg); }
.cs-cell.selected { background: var(--sel-bg); color: var(--sel); font-weight: 600; }
.cs-head.selected { background: var(--sel-bg); color: var(--sel); }
.cel-sheet { user-select: none; touch-action: none; }
.cs-cell { position: relative; }
.cs-cell:focus-visible { outline: 2px solid var(--sel); outline-offset: -2px; }
.cs-help { padding: 6px 10px; color: var(--dim); font-size: 11px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cs-fill-handle { position: absolute; right: 0; bottom: 0; width: 9px; height: 9px;
background: var(--sel); border: 1px solid white; cursor: ns-resize; z-index: 2; }
.cs-preview { outline: 1px dashed var(--sel); outline-offset: -1px; }
/* --------------------------------------------------------------------------
the video -> symbol dialog */
.convert-scrim {
position: fixed;
inset: 0;
z-index: 20;
display: grid;
place-items: center;
background: rgba(0, 0, 0, .35);
}
.convert {
width: min(560px, calc(100vw - 32px));
background: var(--pane);
border: 1px solid var(--line);
border-radius: 3px;
box-shadow: 0 6px 24px rgba(0, 0, 0, .3);
}
.convert-body { display: grid; gap: 8px; padding: 10px; }
.convert-video {
width: 100%;
max-height: 46vh;
background: var(--stage);
border-radius: 2px;
}
/* The range: the kept frames lit, a handle at each end. */
.convert-range {
position: relative;
height: 18px;
margin: 0 7px;
background: var(--sunk);
border: 1px solid var(--hair);
border-radius: 2px;
touch-action: none;
}
.convert-kept {
position: absolute;
top: 0;
bottom: 0;
background: var(--sel-bg);
border-left: 1px solid var(--sel);
border-right: 1px solid var(--sel);
}
.convert-handle {
position: absolute;
top: -3px;
bottom: -3px;
width: 10px;
margin-left: -5px;
background: var(--sel);
border-radius: 2px;
cursor: ew-resize;
}
.convert-name { display: flex; align-items: center; gap: 8px; }
.convert-name input { flex: 1; }
.convert-actions {
display: flex;
justify-content: flex-end;
gap: 6px;
padding: 8px 10px;
border-top: 1px solid var(--hair);
}
/* A transform or visibility channel: its key button, then a field per component. */
.facts dd.channel { display: flex; gap: 3px; align-items: center; overflow: visible; }
.facts dd.channel input[type=number] { width: 0; flex: 1; min-width: 0; }
.facts dd.channel .key { padding: 0 3px; color: var(--dim); }
.facts dd.channel .key.keyed { color: var(--fg); }
.facts dd.channel .key.on { color: var(--sel); }
/* The selected node's transform box, in stage pixels (the SVG's viewBox). */
.paint-overlay .handles .box { fill: none; stroke: #e6ca8b; stroke-width: 0.5; stroke-dasharray: 2 1; pointer-events: none; }
.paint-overlay .handles .knob-arm { stroke: #e6ca8b; stroke-width: 0.5; pointer-events: none; }
.paint-overlay .handles .knob { fill: #161820; stroke: #fff1be; stroke-width: 0.6; cursor: grab; }
.paint-overlay .handles .corner { fill: #fff1be; stroke: #161820; stroke-width: 0.5; cursor: nwse-resize; }
.paint-overlay .handles .pivot { stroke: #fff1be; stroke-width: 0.6; pointer-events: none; }
.paint-overlay:focus { outline: none; }