1812 lines
76 KiB
CSS
1812 lines
76 KiB
CSS
/* Workspace layout: header strip on top, then rail | chips | stage.
|
||
Under 860px the rail becomes a bottom bar and the chips a horizontal strip,
|
||
which is the same shape the phone app uses. */
|
||
|
||
.shell { display: flex; flex-direction: column; height: 100%; }
|
||
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--border-soft);
|
||
background: var(--bg-elev);
|
||
flex-wrap: wrap;
|
||
}
|
||
.brand { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; letter-spacing: 0.04em; color: var(--text); text-decoration: none; }
|
||
.brand img { border-radius: 8px; }
|
||
.brand em { font-style: normal; font-weight: 400; color: var(--accent); }
|
||
/* The page name, top-left beside the mark — the same pair /admin and /profile
|
||
carry, so every screen says where the visitor is. */
|
||
.page-name { font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-dim); }
|
||
.header .spacer { flex: 1 1 auto; }
|
||
/* The preset the studio has open. It wears the theme's own accent rather than
|
||
the dim text colour: it is the one label up there that changes with the
|
||
look, and the same colour the open tab and the brand already carry. */
|
||
.recipe-name {
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
color: var(--accent);
|
||
border: 1px solid var(--border-soft);
|
||
border-radius: 999px;
|
||
padding: 3px 10px;
|
||
max-width: 320px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.menu-wrap { position: relative; }
|
||
|
||
.workspace { display: flex; flex: 1 1 auto; min-height: 0; }
|
||
|
||
/* --- tool rail ---------------------------------------------------------- */
|
||
.rail {
|
||
width: var(--rail-w);
|
||
flex: 0 0 auto;
|
||
border-right: 1px solid var(--border-soft);
|
||
background: var(--bg-sunken);
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 6px;
|
||
gap: 4px;
|
||
overflow-y: auto;
|
||
}
|
||
.rail button {
|
||
border: 1px solid transparent;
|
||
background: transparent;
|
||
border-radius: var(--radius-sm);
|
||
padding: 9px 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 10px;
|
||
letter-spacing: 0.06em;
|
||
color: var(--text-dim);
|
||
}
|
||
.rail button .glyph { font-size: 17px; line-height: 1; }
|
||
.rail button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); }
|
||
.rail button:hover { color: var(--text); }
|
||
/* The build behind the page. It sits at the foot of the column, quiet enough to
|
||
read past, and wraps rather than widening the rail when a name is long. */
|
||
.rail-version {
|
||
margin-top: auto;
|
||
padding: 8px 2px 2px;
|
||
font-family: var(--mono);
|
||
font-size: 9px;
|
||
letter-spacing: 0.02em;
|
||
color: var(--text-dim);
|
||
text-align: center;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
|
||
/* --- panel columns ------------------------------------------------------- */
|
||
/* The panel is a cascade: the rail holds the tabs, then one column per open
|
||
level — the tab's chips, the open chip's sub-chips, the open ruler. A child
|
||
column never hides the column it came from, so the path reads left to right. */
|
||
.chips {
|
||
flex: 0 0 auto;
|
||
max-width: 62vw;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: stretch;
|
||
border-right: 1px solid var(--border-soft);
|
||
background: var(--bg);
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
}
|
||
.col {
|
||
width: var(--col-w);
|
||
flex: 0 0 auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding: 10px;
|
||
overflow-y: auto;
|
||
border-right: 1px solid var(--border-soft);
|
||
}
|
||
.col:last-child { border-right: none; }
|
||
.col-slider { width: var(--slider-w); }
|
||
/* CREATE RECIPES draws a two-up form, so its column is the wide one. */
|
||
.col-create { width: 300px; }
|
||
.col-sub { background: var(--bg-elev); }
|
||
/* RESET is the way out of an edit, not one more chip: a rule and some air keep
|
||
it apart from the tab's own buttons at the foot of the column. */
|
||
.reset-row { margin-top: 6px; padding-top: 12px; border-top: 1px solid var(--border); }
|
||
/* Two things that belong to the phone alone: the path back up under the strip,
|
||
and LIGHT's panels as a chip row. A desktop draws neither — every column is
|
||
in sight, and LIGHT's panels are the column itself. */
|
||
.crumb, .dev-chips { display: none; }
|
||
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
|
||
/* The rule a strip is read across (the mixer's PICK, its bands, the three
|
||
knobs): a line of its own, so it breaks the row it is written into whichever
|
||
direction that row runs — a full width wraps it even in a phone's row-shaped
|
||
column, and `height: 1px` holds it to a line in the desktop column, where the
|
||
row's main axis is the other way round. */
|
||
.chip-rule {
|
||
flex: 0 0 auto;
|
||
width: 100%;
|
||
height: 1px;
|
||
margin: 3px 0;
|
||
background: var(--border-soft);
|
||
}
|
||
/* In a column the chips stack: one chip per row, full width. */
|
||
.col .chip-row { flex-direction: column; align-items: stretch; }
|
||
.col .chip { justify-content: space-between; text-align: left; }
|
||
/* A row of one KIND of value read against each other, not a strip to scan: WB's
|
||
presets, laid out as a table of buttons instead of a wrapping row. */
|
||
.chip-row.grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 6px;
|
||
}
|
||
.chip-row.grid .chip {
|
||
justify-content: center; text-align: center;
|
||
/* Two of these share the column's width, so each is its own small card: the
|
||
standard's name on one line — never wrapped, the names are the point — and
|
||
the kelvin under it, both a size down from a chip's label. */
|
||
flex-direction: column; gap: 1px;
|
||
padding: 4px 6px;
|
||
font-size: 11px; line-height: 1.25;
|
||
white-space: nowrap;
|
||
}
|
||
/* UNDO/REDO are one glyph each, so the pair reads as a single control. */
|
||
.header .btn.icon { padding: 4px 9px; font-size: 15px; line-height: 1.2; }
|
||
/* WATERMARK's own column: its chips carry the longest labels in the app
|
||
("GPS WATERMARK OFF", "CUSTOM WATERMARK OFF" + PRO), which wrap in the
|
||
default column width. */
|
||
.wm-extra { border-right-color: var(--accent-line); width: 200px; }
|
||
/* The watermark panel is two collapses, one per mark: each header chip stands
|
||
on its own controls (colour, face, switches, fields), and a rule keeps the
|
||
two apart while both marks are on. */
|
||
.wm-sec { display: flex; flex-direction: column; gap: 6px; }
|
||
.wm-sec + .wm-sec { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }
|
||
.chip {
|
||
border: 1px solid var(--border);
|
||
background: var(--bg-elev);
|
||
border-radius: 999px;
|
||
padding: 5px 11px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.col-sub .chip { background: var(--bg); }
|
||
/* The consent tick in MY PHOTOS' history column: a plain checkbox, not a chip —
|
||
it is a permission, and it reads as one. */
|
||
.consent {
|
||
display: flex; align-items: flex-start; gap: 7px;
|
||
margin-top: 10px; padding-top: 10px;
|
||
border-top: 1px solid var(--border);
|
||
font-size: 12px; line-height: 1.4; color: var(--text-dim);
|
||
cursor: pointer;
|
||
}
|
||
.consent input { flex: 0 0 auto; margin: 1px 0 0; accent-color: var(--accent); }
|
||
/* SAVE RECENT's own files: the name fills the row, the ✕ that deletes it sits
|
||
beside it — the account's recipe list is the one place a file can be thrown
|
||
away from the studio. */
|
||
.saved-list { display: flex; flex-direction: column; gap: 6px; }
|
||
.saved-row { display: flex; gap: 6px; align-items: stretch; }
|
||
.saved-row .chip:first-child { flex: 1; justify-content: space-between; text-align: left; }
|
||
.chip[aria-pressed="true"], .chip.on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
|
||
/* A chip painted with the value it picks (a WB preset's cast, the same swatch
|
||
the ruler paints). The fill and the ink are inline — they ARE the value, not
|
||
the theme — so both are stated by the caller; the border is the chip's own
|
||
ink, which is visible against the fill it was chosen for, and the accent once
|
||
the preset is on, since the fill can no longer say so. */
|
||
.chip.tinted {
|
||
border: 2px solid currentColor;
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
}
|
||
.chip.tinted.on { border-color: var(--accent); }
|
||
/* The kelvin rides on a swatch, whose ink was picked for that fill to clear
|
||
4.5:1 — so a value slot there wears that ink and not the panel's dim grey,
|
||
which is what a mid grey button cannot carry. It is set a size down from the
|
||
name instead, which is separation that costs no contrast. */
|
||
.chip.tinted .val { color: inherit; }
|
||
/* Edited value, strip closed: the text glows amber, the border does not. */
|
||
.chip.amber, .chip.amber .val { color: var(--accent); }
|
||
.chip .val { color: var(--text-dim); font-family: var(--mono); font-size: 11px; }
|
||
.chip-row.grid .chip .val { font-size: 10px; }
|
||
.chip .pro, .btn .pro { font-size: 9px; letter-spacing: 0.08em; color: var(--accent); }
|
||
/* A button is not a flex row the way a chip is, so the marker carries its own
|
||
gap off the label it qualifies. */
|
||
.btn .pro { margin-left: 6px; }
|
||
.chip:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
/* The font group as a dropdown: one row that shows the current pairing instead
|
||
of three chips that all have to fit. */
|
||
.font-select {
|
||
display: block;
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
padding-right: 26px;
|
||
appearance: none;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%239aa2ad' stroke-width='1.5'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat;
|
||
background-position: right 10px center;
|
||
}
|
||
.font-select option { background: var(--bg-elev); color: var(--text); }
|
||
|
||
/* The ruler column: "<" back button, label + live value, slider, colour swatch
|
||
for the knobs that drive a hue (WB's TEMPERATURE / TINT). */
|
||
.slider-row { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 10px; display: flex; align-items: center; gap: 10px; }
|
||
.col .slider-row { flex-direction: column; align-items: stretch; gap: 6px; }
|
||
.slider-row .ruler { flex: 1 1 auto; min-width: 0; }
|
||
/* The same one-line ruler as the sidebar's rows: the knob's name, its track,
|
||
then the number it is on — and a colour swatch under them where the knob
|
||
drives a hue (WB's TEMPERATURE / TINT). */
|
||
.slider-row .head { display: flex; align-items: center; gap: 8px; font-size: 11px; letter-spacing: 0.1em; color: var(--text-dim); }
|
||
.slider-row .head span { flex: 0 0 auto; }
|
||
.slider-row .head input[type="range"] { flex: 1 1 40px; min-width: 0; }
|
||
.slider-row .head b { flex: 0 0 auto; color: var(--text); font-family: var(--mono); letter-spacing: 0; }
|
||
.slider-row .head b.amber { color: var(--accent); }
|
||
.slider-row .swatch {
|
||
height: 26px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.slider-row .swatch span {
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.06em;
|
||
color: #fff;
|
||
mix-blend-mode: difference;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.slider-row .back {
|
||
flex: 0 0 auto;
|
||
align-self: flex-start;
|
||
border: 1px solid var(--border);
|
||
background: var(--bg-elev);
|
||
color: var(--text-dim);
|
||
border-radius: 999px;
|
||
padding: 4px 10px;
|
||
font-family: var(--mono);
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
input[type="range"] { width: 100%; accent-color: var(--accent); }
|
||
.hint { color: var(--text-dim); font-size: 12px; line-height: 1.5; }
|
||
.hint.ok { color: var(--accent); }
|
||
|
||
/* --- PRO gate bar ------------------------------------------------------- */
|
||
/* One line between the header and the workspace, shown only while a signed-in
|
||
account has not proven its address — it is served as a guest until it does. */
|
||
.verify-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
padding: 6px 12px;
|
||
background: var(--accent-soft);
|
||
border-bottom: 1px solid var(--accent-line);
|
||
font-size: 12px;
|
||
}
|
||
.verify-bar .btn { padding: 3px 10px; font-size: 11px; }
|
||
/* The trial notice: the verify bar's shape in the plain surface rather than the
|
||
accent — it is news, and it offers nothing to click. */
|
||
.notice-bar {
|
||
padding: 6px 12px;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
background: var(--bg-elev);
|
||
border-bottom: 1px solid var(--border);
|
||
color: var(--text);
|
||
}
|
||
|
||
/* --- CREATE RECIPES form (the phone's RecipeCreateModal, unrolled) ------- */
|
||
.create-form {
|
||
border-top: 1px solid var(--border-soft);
|
||
padding-top: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.create-label {
|
||
font-size: 10px;
|
||
letter-spacing: 0.12em;
|
||
color: var(--text-dim);
|
||
font-weight: 600;
|
||
}
|
||
.create-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
||
/* Collapsible groups: native <details>, so only the caret is ours. */
|
||
.create-sec { display: flex; flex-direction: column; gap: 6px; }
|
||
.create-sec > summary { display: flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; user-select: none; }
|
||
.create-sec > summary::-webkit-details-marker { display: none; }
|
||
.create-sec > summary::before { content: '▸'; font-size: 9px; transition: transform 0.15s ease; }
|
||
.create-sec[open] > summary::before { transform: rotate(90deg); }
|
||
.create-num { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
||
.create-num span { font-size: 10px; letter-spacing: 0.12em; color: var(--text-dim); font-weight: 600; }
|
||
.create-num.wide { grid-column: 1 / -1; }
|
||
.create-shift { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||
.create-shift .field { width: 64px; }
|
||
.create-actions { display: flex; gap: 6px; margin-top: 4px; }
|
||
.create-actions .btn { flex: 1 1 auto; }
|
||
|
||
/* --- stage -------------------------------------------------------------- */
|
||
.stage {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
background: var(--bg-sunken);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
position: relative;
|
||
}
|
||
.dropzone {
|
||
border: 2px dashed var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 40px 28px;
|
||
text-align: center;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
background: var(--bg);
|
||
max-width: 520px;
|
||
}
|
||
.dropzone.over { border-color: var(--accent); color: var(--text); background: var(--accent-soft); }
|
||
.dropzone strong { display: block; color: var(--text); margin-bottom: 6px; font-size: 16px; }
|
||
/* The camera sits beside the picker's own hint, so it needs a line of its own
|
||
rather than riding on the sentence above it. The click on this button must
|
||
not also open the file dialog the dropzone itself listens for. */
|
||
.dropzone .btn { margin-top: 14px; }
|
||
.canvas-wrap { position: relative; display: flex; align-items: center; justify-content: center; min-height: 0; flex: 1 1 auto; width: 100%; overflow: hidden; }
|
||
.canvas-wrap img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: var(--radius-sm); box-shadow: var(--shadow); }
|
||
/* Crop editor: the layer is the photo's own box, the rect is the framing and
|
||
its shadow is what shades the part being cut away. */
|
||
/* Compare: the original left of the split, the grade right of it. The layer is
|
||
the photo's own box and carries the stage's transform, so the bar stays put
|
||
on the photo whatever the zoom or pan. */
|
||
.compare-layer {
|
||
position: absolute;
|
||
overflow: hidden;
|
||
border-radius: var(--radius-sm);
|
||
cursor: ew-resize;
|
||
touch-action: none;
|
||
}
|
||
.compare-orig { position: absolute; inset: 0; width: 100%; height: 100%; box-shadow: none; }
|
||
.compare-bar {
|
||
position: absolute;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 2px;
|
||
margin-left: -1px;
|
||
background: #fff;
|
||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
|
||
pointer-events: none;
|
||
}
|
||
.compare-bar::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
width: 30px;
|
||
height: 30px;
|
||
transform: translate(-50%, -50%);
|
||
border-radius: 999px;
|
||
background: rgba(255, 255, 255, 0.94);
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.crop-layer { position: absolute; overflow: hidden; border-radius: var(--radius-sm); pointer-events: none; }
|
||
.crop-rect { position: absolute; border: 1px solid var(--accent); box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5); cursor: move; pointer-events: auto; touch-action: none; }
|
||
.crop-handle { position: absolute; width: 16px; height: 16px; background: var(--accent); border: 2px solid #fff; border-radius: 3px; touch-action: none; }
|
||
.crop-handle.tl { left: 0; top: 0; transform: translate(-50%, -50%); cursor: nwse-resize; }
|
||
.crop-handle.tr { right: 0; top: 0; transform: translate(50%, -50%); cursor: nesw-resize; }
|
||
.crop-handle.bl { left: 0; bottom: 0; transform: translate(-50%, 50%); cursor: nesw-resize; }
|
||
.crop-handle.br { right: 0; bottom: 0; transform: translate(50%, 50%); cursor: nwse-resize; }
|
||
/* LIGHT's TONE CURVE: the graph hung on the photo. The layer is the photo's own
|
||
box and takes no pointer events; the card sits at its bottom-left corner and
|
||
takes them all, because every gesture in it is about the curve. */
|
||
.curve-layer { position: absolute; pointer-events: none; }
|
||
.curve-panel {
|
||
position: absolute;
|
||
left: 10px;
|
||
bottom: 10px;
|
||
pointer-events: auto;
|
||
touch-action: none;
|
||
width: 240px;
|
||
padding: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
box-shadow: var(--shadow);
|
||
user-select: none;
|
||
}
|
||
.curve-head { display: flex; align-items: center; justify-content: space-between; cursor: grab; }
|
||
.curve-panel.dragging .curve-head { cursor: grabbing; }
|
||
.curve-title { font-size: 11px; letter-spacing: 0.1em; color: var(--muted); }
|
||
.curve-close {
|
||
border: none;
|
||
background: none;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
padding: 2px 4px;
|
||
cursor: pointer;
|
||
}
|
||
.curve-close:hover { color: var(--fg); }
|
||
.curve-tabs { display: flex; gap: 4px; align-items: center; }
|
||
.curve-tab {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
flex: 1 1 0;
|
||
justify-content: center;
|
||
padding: 3px 0;
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.06em;
|
||
color: var(--muted);
|
||
background: none;
|
||
border: 1px solid var(--border);
|
||
border-radius: 3px;
|
||
cursor: pointer;
|
||
}
|
||
.curve-tab.on { border-color: var(--accent-line); color: var(--fg); }
|
||
/* The dot carries the channel's own colour, so the tab says which line it draws;
|
||
it goes amber once that channel has left the diagonal. */
|
||
.curve-dot { width: 7px; height: 7px; border-radius: 999px; }
|
||
.curve-tab.moved .curve-dot { box-shadow: 0 0 0 2px var(--accent); }
|
||
.curve-reset {
|
||
flex: 0 0 auto;
|
||
padding: 3px 7px;
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.06em;
|
||
color: var(--muted);
|
||
background: none;
|
||
border: 1px solid var(--border);
|
||
border-radius: 3px;
|
||
cursor: pointer;
|
||
}
|
||
.curve-reset:hover { color: var(--fg); border-color: var(--accent-line); }
|
||
.curve-graph { display: block; background: rgba(0, 0, 0, 0.35); border: 1px solid var(--border); border-radius: 3px; cursor: crosshair; }
|
||
/* The picture's own distribution, the floor the graph is read against. Light on
|
||
both themes because the graph itself is always the dark box over a photo. */
|
||
.curve-hist { fill: rgba(255, 255, 255, 0.22); }
|
||
.curve-grid { stroke: rgba(255, 255, 255, 0.1); stroke-width: 1; }
|
||
.curve-diagonal { stroke: rgba(255, 255, 255, 0.28); stroke-width: 1; stroke-dasharray: 3 3; }
|
||
/* The line the active tab draws, and — on a channel tab — the composite curve
|
||
under it, so the two moves can be told apart. */
|
||
.curve-line { fill: none; stroke-width: 2; stroke-linecap: round; }
|
||
.curve-line.ghost { stroke: rgba(255, 255, 255, 0.35); stroke-width: 1; stroke-dasharray: 4 3; }
|
||
.curve-point { fill: var(--bg-elev); stroke-width: 2; cursor: grab; }
|
||
.curve-point:active { cursor: grabbing; }
|
||
|
||
/* FRAME's custom watermark box: drag the body to move, the corner to scale. */
|
||
.wm-layer { position: absolute; border-radius: var(--radius-sm); pointer-events: none; }
|
||
.wm-rect { position: absolute; border: 1px dashed var(--accent); cursor: move; pointer-events: auto; touch-action: none; }
|
||
.wm-handle { position: absolute; width: 14px; height: 14px; background: var(--accent); border: 2px solid #fff; border-radius: 3px; touch-action: none; }
|
||
.wm-handle.br { right: 0; bottom: 0; transform: translate(50%, 50%); cursor: nwse-resize; }
|
||
/* A watermark's colour: the preset chips are a shortcut, the well picks any. */
|
||
.color-well { display: flex; align-items: center; gap: 8px; padding: 6px 8px; cursor: pointer; }
|
||
.color-well input[type="color"] { width: 34px; height: 22px; padding: 0; border: 1px solid var(--accent-line); border-radius: 3px; background: none; cursor: pointer; }
|
||
.color-well .val { font-size: 11px; letter-spacing: 0.08em; color: var(--muted); }
|
||
/* Eyedropper: the photo's own box, left transparent, with a crosshair cursor. */
|
||
.pick-layer {
|
||
position: absolute;
|
||
border-radius: var(--radius-sm);
|
||
cursor: crosshair;
|
||
touch-action: none;
|
||
}
|
||
/* FX's brush — HEAL and MOSAIC, one layer. It takes the pointer the way the
|
||
eyedropper's does, so a click paints and a drag paints a run instead of
|
||
panning; the circle under the cursor is the size the wheel has set, the trail
|
||
is the stroke being drawn, and every spot on the photo keeps its own circle.
|
||
A HEAL spot shows the pair the renderer works with — the hole it fills, and
|
||
the patch it borrowed; a MOSAIC spot has nothing beside it, since what it
|
||
covers is gone. */
|
||
.brush-layer {
|
||
position: absolute;
|
||
border-radius: var(--radius-sm);
|
||
cursor: none;
|
||
touch-action: none;
|
||
}
|
||
.brush-set { position: absolute; inset: 0; pointer-events: none; }
|
||
.brush-cursor,
|
||
.brush-spot,
|
||
.brush-source {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
transform: translate(-50%, -50%);
|
||
pointer-events: none;
|
||
}
|
||
.brush-cursor { border: 1px solid rgba(255, 255, 255, 0.9); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55); }
|
||
/* The stroke in progress is one band of the brush's own width — a path with
|
||
round ends and round joins — so a drag reads as a brush being drawn across
|
||
the photo rather than as the beads a row of overlapping circles looks like.
|
||
It wears the accent, so the run reads as something about to happen rather
|
||
than a spot already laid. */
|
||
.brush-trail {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: visible;
|
||
pointer-events: none;
|
||
}
|
||
.brush-trail path {
|
||
fill: none;
|
||
stroke: var(--accent);
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
stroke-opacity: 0.45;
|
||
}
|
||
.brush-spot { border: 1px solid rgba(255, 255, 255, 0.85); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45); }
|
||
.brush-source { border: 1px dashed rgba(255, 255, 255, 0.7); }
|
||
/* The spots a stroke left are drawn as the one mark it was painted as: a band
|
||
of the brush's own width through them, in the same translucent white with the
|
||
same dark hairline the circles wear, so the band reads on a white photo too.
|
||
The dashed one is the patch HEAL borrowed, which is a band of its own and is
|
||
meant to look borrowed. A stroke is one path, so where it crosses itself its
|
||
translucency is laid once rather than twice — that doubling-up is exactly
|
||
what a row of overlapping circles looks like. */
|
||
.brush-band {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: visible;
|
||
pointer-events: none;
|
||
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.45));
|
||
}
|
||
.brush-band path {
|
||
fill: none;
|
||
stroke: rgba(255, 255, 255, 0.18);
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
}
|
||
.brush-band.band-src path { stroke: rgba(255, 255, 255, 0.12); stroke-dasharray: 5 5; }
|
||
/* A spot the band stands for: its box on the photo is still the spot's, its own
|
||
edge is the band's now. */
|
||
.brush-spot[data-band],
|
||
.brush-source[data-band] { border-color: transparent; box-shadow: none; }
|
||
/* A repair the pointer is not on and no press has chosen: the ring would be one
|
||
more circle on a photo that already carries as many repairs as the user laid,
|
||
so an idle spot is the patch it borrowed and a faint print of the place it
|
||
mended — soft, because it is a record of an edit rather than something to
|
||
aim at, and it is the ring under the pointer that says the spot can be taken
|
||
hold of again. */
|
||
.brush-spot[data-idle] {
|
||
border-color: transparent;
|
||
background: rgba(127, 127, 127, 0.14);
|
||
box-shadow: 0 0 7px 2px rgba(0, 0, 0, 0.25);
|
||
filter: blur(1px);
|
||
}
|
||
/* A run the band already stands for needs no print of its own under it. */
|
||
.brush-spot[data-idle][data-band] { background: none; box-shadow: none; filter: none; }
|
||
/* In hand, one of the run's spots is the circle it would be taken hold of by, so
|
||
it wears that circle again over the band — the ring is what says the spot is
|
||
under the pointer. */
|
||
.brush-spot[data-band]:not([data-idle]) { border-color: rgba(255, 255, 255, 0.85); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45); }
|
||
/* The × on the spot the pointer chose. It sits on the spot's corner and is the
|
||
one thing on this layer the pointer is meant to click, so it takes the
|
||
pointer back off the paint. */
|
||
.brush-del {
|
||
position: absolute;
|
||
transform: translate(-50%, -50%);
|
||
width: 18px;
|
||
height: 18px;
|
||
padding: 0;
|
||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||
border-radius: 50%;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
color: #fff;
|
||
font: 12px/1 sans-serif;
|
||
cursor: pointer;
|
||
pointer-events: auto;
|
||
}
|
||
.brush-del:hover { background: var(--accent); }
|
||
/* FX's gradient masks — LINEAR and RADIAL GRADIENT, one layer. It takes the
|
||
pointer while a mask tool is armed, so a drag draws the next shape instead of
|
||
panning the photo; the shapes already on the photo answer only through their
|
||
own pin and handles, which is what lets a press mean "take hold of this one"
|
||
rather than "draw another". */
|
||
.mask-layer {
|
||
position: absolute;
|
||
border-radius: var(--radius-sm);
|
||
cursor: crosshair;
|
||
touch-action: none;
|
||
}
|
||
/* The shape's own outline, over the photo: the pin and the handles are the
|
||
things on this layer to press, and the chosen shape's edge is one more — the
|
||
line a ramp falls along, or an ellipse's rim. Invisible and a finger wide, so
|
||
a press on the edge turns the shape without having to find a handle for it;
|
||
non-scaling, so the band is the same width however the photo is zoomed. */
|
||
.mask-shape {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: visible;
|
||
pointer-events: none;
|
||
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.45));
|
||
}
|
||
.mask-line { stroke: rgba(255, 255, 255, 0.85); stroke-width: 1; }
|
||
.mask-edge {
|
||
fill: none;
|
||
stroke: rgba(255, 255, 255, 0.01);
|
||
stroke-width: 16;
|
||
pointer-events: stroke;
|
||
vector-effect: non-scaling-stroke;
|
||
cursor: grab;
|
||
}
|
||
/* An ellipse's rim, and inside it the ellipse its feather fades to — so the
|
||
hand the shape has on the photo is visible before the knobs are moved. */
|
||
.mask-ring { fill: none; stroke: rgba(255, 255, 255, 0.85); stroke-width: 1; }
|
||
.mask-ring.feather { stroke: rgba(255, 255, 255, 0.5); stroke-dasharray: 4 4; }
|
||
/* The pin that moves the whole shape, and the handles that resize it. Both are
|
||
the pointer's own, and both keep a screen size of their own whatever the zoom
|
||
(the layer's transform is taken back out of them). */
|
||
.mask-pin,
|
||
.mask-handle {
|
||
position: absolute;
|
||
width: 13px;
|
||
height: 13px;
|
||
border: 1px solid rgba(0, 0, 0, 0.55);
|
||
background: rgba(255, 255, 255, 0.9);
|
||
pointer-events: auto;
|
||
cursor: move;
|
||
}
|
||
.mask-pin { border-radius: 50%; }
|
||
.mask-handle { border-radius: 2px; }
|
||
.mask-handle.rot { border-radius: 50%; cursor: grab; }
|
||
/* The mixer's readout: the sampled swatch, its RGB and HSL, and the band the
|
||
ruler is pointed at. */
|
||
.hsl-readout {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
margin-top: 8px;
|
||
padding-top: 10px;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
.hsl-swatch { display: block; height: 34px; border: 1px solid var(--border); border-radius: var(--radius-sm); }
|
||
.hsl-line { font-size: 12px; color: var(--text-dim); font-variant-numeric: tabular-nums; }
|
||
/* A chip that names a colour shows it: the dot rides the leading edge. */
|
||
.chip-dot { width: 10px; height: 10px; border-radius: 999px; border: 1px solid var(--border); flex: 0 0 auto; }
|
||
/* The eyedropper's own icon, riding the cursor inside the pick layer. Its hot
|
||
spot is the tip, so it is nudged up and left of the pointer. */
|
||
.pick-icon { position: absolute; pointer-events: none; transform: translate(-2px, -21px); filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.65)); }
|
||
.pick-icon-body { fill: #fff; stroke: #111; stroke-width: 1.2; }
|
||
.pick-icon-tip { fill: var(--accent); stroke: #111; stroke-width: 1.2; }
|
||
/* The mixer's panel for the colour just sampled: it hangs on the photo at the
|
||
point the eyedropper read, its swatch on top and HUE/SAT/LUM in one row
|
||
under it. The layer is transparent to the pointer, the card is not. */
|
||
.hsl-panel-layer { position: absolute; pointer-events: none; }
|
||
.hsl-panel {
|
||
position: absolute;
|
||
pointer-events: auto;
|
||
/* Dragged by its body to a clear patch of photo; the knobs and the close
|
||
button keep the pointer to themselves. */
|
||
cursor: grab;
|
||
touch-action: none;
|
||
width: 248px;
|
||
padding: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.hsl-card-head { display: flex; align-items: center; gap: 8px; }
|
||
.hsl-card-swatch { width: 28px; height: 28px; flex: 0 0 auto; border: 1px solid var(--border); border-radius: var(--radius-sm); }
|
||
.hsl-card-name { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; font-size: 11px; letter-spacing: 0.08em; color: var(--text-dim); }
|
||
.hsl-card-name b { font-family: var(--mono); font-size: 12px; letter-spacing: 0; color: var(--text); }
|
||
.hsl-card-close { border: 1px solid var(--border); background: var(--bg); color: var(--text-dim); border-radius: 999px; padding: 2px 7px; font-size: 11px; cursor: pointer; }
|
||
.hsl-card-knobs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||
.mini-slider { min-width: 0; }
|
||
.mini-head { display: flex; align-items: center; gap: 8px; font-size: 10px; letter-spacing: 0.08em; color: var(--text-dim); }
|
||
.mini-head span { flex: 0 0 auto; }
|
||
/* The track takes the width the two labels leave, so the name of the knob opens
|
||
the row and the number it is on closes it (see MiniSlider). */
|
||
.mini-head input[type="range"] { flex: 1 1 40px; min-width: 0; }
|
||
.mini-head b { flex: 0 0 auto; font-family: var(--mono); letter-spacing: 0; color: var(--text); }
|
||
.mini-head b.amber { color: var(--accent); }
|
||
/* The mixer's three knobs share one narrow panel, so they keep the stacked form
|
||
the row above has just left: the name and its number on one line, the track
|
||
under them. The readout is ordered ahead of the track, which takes the whole
|
||
second line — in the markup the track sits between the two. */
|
||
.hsl-card-knobs .mini-head { flex-wrap: wrap; justify-content: space-between; }
|
||
.hsl-card-knobs .mini-head b { order: 1; }
|
||
.hsl-card-knobs .mini-head input[type="range"] { order: 2; flex: 1 0 100%; }
|
||
/* FRAME's STRAIGHTEN scale: a graduated ruler lying along the foot of the
|
||
photo. The layer is transparent to the pointer so the photo still answers a
|
||
drag outside the card; the card itself takes the drag. */
|
||
.straighten-layer { position: absolute; z-index: 3; pointer-events: none; }
|
||
.straighten-ruler {
|
||
position: absolute;
|
||
left: 10px;
|
||
right: 10px;
|
||
bottom: 10px;
|
||
padding: 6px 14px 8px;
|
||
pointer-events: auto;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
background: transparent;
|
||
user-select: none;
|
||
touch-action: none;
|
||
}
|
||
.straighten-head { display: flex; align-items: baseline; justify-content: space-between; font-size: 10px; letter-spacing: 0.08em; color: var(--text-dim); text-shadow: 0 1px 2px var(--bg); }
|
||
.straighten-head b { font-family: var(--mono); font-size: 12px; letter-spacing: 0; color: var(--text); font-variant-numeric: tabular-nums; }
|
||
.straighten-head b.amber { color: var(--accent); }
|
||
.straighten-scale { position: relative; height: 30px; cursor: ew-resize; filter: drop-shadow(0 0 2px var(--bg)); }
|
||
.straighten-tick { position: absolute; bottom: 12px; width: 1px; height: 7px; background: var(--border); transform: translateX(-50%); }
|
||
.straighten-tick.major { height: 12px; background: var(--text-dim); }
|
||
.straighten-tick.zero { height: 16px; background: var(--text); }
|
||
.straighten-tick i { position: absolute; top: 13px; left: 0; transform: translateX(-50%); font-style: normal; font-size: 9px; color: var(--text-dim); }
|
||
.straighten-needle { position: absolute; bottom: 4px; width: 2px; height: 20px; background: var(--accent); transform: translateX(-50%); pointer-events: none; }
|
||
/* Histogram overlay: a small translucent frame on the photo, dragged by its
|
||
header. The luma curve is a grey fill behind the three channel curves, which
|
||
screen over each other the way a Lightroom histogram reads. */
|
||
.hist {
|
||
position: absolute;
|
||
z-index: 4;
|
||
width: 236px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
background: color-mix(in srgb, var(--bg) 86%, transparent);
|
||
backdrop-filter: blur(6px);
|
||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
|
||
user-select: none;
|
||
touch-action: none;
|
||
}
|
||
.hist-head { display: flex; align-items: center; gap: 4px; padding: 3px 4px 3px 8px; cursor: grab; }
|
||
.hist.dragging .hist-head { cursor: grabbing; }
|
||
.hist-title { flex: 1 1 auto; font-size: 10px; letter-spacing: 0.08em; color: var(--text-dim); }
|
||
.hist-close {
|
||
width: 18px; height: 18px; padding: 0; line-height: 1;
|
||
font-size: 13px; color: var(--text-dim);
|
||
background: none; border: 0; border-radius: 4px; cursor: pointer;
|
||
}
|
||
.hist-close:hover { background: var(--bg-sunken); color: var(--text); }
|
||
.hist-plot { display: block; width: 100%; height: 84px; }
|
||
.hist-lum { fill: var(--text-dim); opacity: 0.38; }
|
||
.hist-ch { fill: none; stroke-width: 1.1; }
|
||
/* Screen is how the three curves add up like light on a dark panel — but it is
|
||
measured against the backdrop, so on the light theme (a white plot) it
|
||
screens every channel straight to white and the histogram reads as empty. */
|
||
[data-theme='dark'] .hist-ch { mix-blend-mode: screen; }
|
||
.hist-r { stroke: #ff5a5a; }
|
||
.hist-g { stroke: #4ade80; }
|
||
.hist-b { stroke: #5aa2ff; }
|
||
.hist-axis {
|
||
display: flex; justify-content: space-between;
|
||
padding: 1px 6px 4px;
|
||
font-family: var(--mono); font-size: 9px; color: var(--text-dim);
|
||
}
|
||
.busy {
|
||
position: absolute;
|
||
inset: auto 0 12px 0;
|
||
margin: 0 auto;
|
||
width: fit-content;
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--border);
|
||
border-radius: 999px;
|
||
padding: 5px 12px;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.stage .toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: center; }
|
||
/* The photo's own size, first in that row: monospaced so the digits do not
|
||
shuffle the buttons sideways while a crop is being dragged. */
|
||
.stage .toolbar .dims { font-family: var(--mono); white-space: nowrap; }
|
||
.err { color: var(--danger); font-size: 12px; max-width: 520px; text-align: center; }
|
||
|
||
/* The stage and, over it, the live viewfinder: the slot they share in the
|
||
workspace. The columns beside it are the app's own — a slider there moves the
|
||
look on the frames the camera is grading right now. */
|
||
.stage-slot { flex: 1 1 auto; min-width: 0; display: flex; position: relative; }
|
||
|
||
/* --- live viewfinder ---------------------------------------------------- */
|
||
/* Covers the stage, not the app: black whatever the theme, because a viewfinder
|
||
is something the user looks through, but the tabs stay in reach — that is the
|
||
whole point of framing through the recipe. */
|
||
.camera-view {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 5;
|
||
background: #000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
}
|
||
/* The graded frame itself. `object-fit: contain` keeps the whole picture on
|
||
screen at any window shape, which is the composition the export writes. */
|
||
.camera-feed {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
object-fit: contain;
|
||
background: #000;
|
||
}
|
||
.camera-bar,
|
||
.camera-deck {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
width: 100%;
|
||
max-width: 900px;
|
||
}
|
||
.camera-bar .hint { color: #a9a9a9; font-family: var(--mono); }
|
||
.camera-bar .spacer { flex: 1 1 auto; }
|
||
.camera-deck .camera-spacer { width: 44px; }
|
||
.camera-err { max-width: 900px; }
|
||
/* The <video> the live frames are read from. It has to be IN the document —
|
||
Safari paints blank frames from a detached video — but never seen: 1px, faint
|
||
and out of the way, behind the black backdrop. Not `display: none`: that stops
|
||
the frames as surely as detaching it. */
|
||
.camera-source-host {
|
||
position: fixed;
|
||
left: 0;
|
||
bottom: 0;
|
||
width: 1px;
|
||
height: 1px;
|
||
opacity: 0.01;
|
||
overflow: hidden;
|
||
pointer-events: none;
|
||
}
|
||
.camera-source-host video { width: 1px; height: 1px; }
|
||
/* The shutter: the app's one round control, because it is the one control whose
|
||
shape says what it does. */
|
||
.shutter {
|
||
width: 62px;
|
||
height: 62px;
|
||
margin: 0 auto;
|
||
padding: 0;
|
||
border: 3px solid #fff;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.65);
|
||
cursor: pointer;
|
||
}
|
||
.shutter:disabled { opacity: 0.4; cursor: default; }
|
||
|
||
/* --- auth dialog -------------------------------------------------------- */
|
||
.modal-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 60;
|
||
padding: 16px;
|
||
}
|
||
.modal {
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 20px;
|
||
width: 100%;
|
||
max-width: 360px;
|
||
box-shadow: var(--shadow);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.modal h2 { margin: 0; font-size: 17px; }
|
||
/* The one row in the export menu that is not a plain button: a number to type
|
||
and the button that takes it. */
|
||
.export-custom {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.export-custom .field { width: 96px; }
|
||
|
||
/* --- strip moderation (/admin) ------------------------------------------ */
|
||
.adm { height: 100%; overflow-y: auto; padding: 0 22px 40px; }
|
||
/* Sticky: the mark and the page name stay in the top-left corner while the
|
||
photo grid scrolls under them, like the studio's header and the landing nav. */
|
||
.adm-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 5;
|
||
margin: 0 -22px 16px;
|
||
padding: 12px 22px;
|
||
background: var(--bg);
|
||
border-bottom: 1px solid var(--border-soft);
|
||
}
|
||
.adm-title { font-size: 15px; letter-spacing: 0.02em; }
|
||
/* The mark and the page name, top-left, always a way back to the landing page —
|
||
the same pair the studio's TopBar carries. */
|
||
.adm-brand { display: flex; align-items: center; gap: 6px; font-weight: 650; letter-spacing: -0.01em; color: var(--text); text-decoration: none; }
|
||
.adm-brand em { font-style: normal; font-weight: 400; color: var(--accent); }
|
||
.adm-brand img { border-radius: 8px; }
|
||
/* Curating pictures: two album columns of the same shape — the uploads (one
|
||
album per account) and the landing (one album per section). Each column
|
||
lists its albums and then draws the open album's frames. */
|
||
.adm-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: start; }
|
||
@media (max-width: 1100px) { .adm-cols { grid-template-columns: minmax(0, 1fr); } }
|
||
.adm-col { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
|
||
/* The filters over the album list: one name box, two pickers. */
|
||
.adm-input {
|
||
width: 100%;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
color: var(--text);
|
||
background: var(--bg-sunken);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 6px 8px;
|
||
}
|
||
.adm-filters { display: flex; gap: 6px; }
|
||
.adm-filters .adm-input { min-width: 0; }
|
||
/* A shelf of albums: it scrolls on its own so the frames below stay reachable
|
||
however many accounts or sections there are. */
|
||
.adm-album-list { display: flex; flex-direction: column; gap: 4px; max-height: 46vh; overflow-y: auto; padding-right: 2px; }
|
||
.adm-album {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
text-align: left;
|
||
color: var(--text-dim);
|
||
background: var(--bg-sunken);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 7px 9px;
|
||
cursor: pointer;
|
||
}
|
||
.adm-album:hover { border-color: var(--accent-line); }
|
||
.adm-album.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
|
||
.adm-album-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.adm-album-count { font-family: var(--mono); font-size: 11px; }
|
||
/* One album column: the shelf of albums down the left, the frame that is up
|
||
beside it, the album's strip of thumbnails across the bottom. */
|
||
.adm-split { display: grid; grid-template-columns: minmax(150px, 200px) minmax(0, 1fr); gap: 12px; align-items: start; }
|
||
.adm-stage {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: var(--bg);
|
||
padding: 10px;
|
||
}
|
||
.adm-stage-img { width: 100%; aspect-ratio: 4 / 3; object-fit: contain; border-radius: var(--radius-sm); background: var(--bg-sunken); }
|
||
/* The album's strip: thumbnails in one row, scrolled sideways when they
|
||
overflow the column rather than wrapping into a wall of pictures. */
|
||
.adm-filmstrip { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
|
||
.adm-thumb {
|
||
flex: 0 0 132px;
|
||
margin: 0;
|
||
padding: 0;
|
||
font: inherit;
|
||
cursor: pointer;
|
||
border: 1px solid var(--border-soft);
|
||
border-radius: var(--radius-sm);
|
||
overflow: hidden;
|
||
background: var(--bg-sunken);
|
||
color: var(--text-dim);
|
||
}
|
||
.adm-thumb:hover { border-color: var(--accent-line); }
|
||
.adm-thumb.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
||
.adm-thumb img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }
|
||
.adm-thumb-cap { display: block; padding: 3px 6px; font-family: var(--mono); font-size: 10.5px; }
|
||
/* The three destinations sit on one line under the photo, each an independent
|
||
checkbox — tick all three and the photo shows in all three sections. */
|
||
.adm-choices { display: flex; flex-wrap: wrap; gap: 5px; }
|
||
.adm-choice {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
font: inherit;
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
color: var(--text-dim);
|
||
background: var(--bg-sunken);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
.adm-choice:hover { border-color: var(--accent-line); }
|
||
.adm-choice:has(input:checked) { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
|
||
.adm-choice:has(input:disabled) { opacity: 0.5; cursor: default; }
|
||
.adm-choice input { margin: 0; accent-color: var(--accent); }
|
||
.adm-row-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin: 0 0 8px;
|
||
font-size: 11.5px;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--text-dim);
|
||
}
|
||
.adm-row-count { font-family: var(--mono); color: var(--text); }
|
||
.adm-row-empty { margin: 0; }
|
||
/* The QR section's real artefact is the code, not the photo. Drawn on white:
|
||
a scanner needs the quiet zone, and the dark theme has none. */
|
||
.adm-qr { display: flex; align-items: center; justify-content: center; min-height: 88px; padding: 6px; background: #fff; }
|
||
.adm-qr img { width: auto; aspect-ratio: 1 / 1; }
|
||
.adm-qr span { color: #6b7280; font-size: 10.5px; text-align: center; }
|
||
.adm-spacer { flex: 1 1 auto; }
|
||
.adm-note { margin: 12px 0; }
|
||
.adm-sub { margin: 0 0 14px; }
|
||
.adm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
||
.adm-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: var(--bg-elev);
|
||
padding: 10px;
|
||
}
|
||
.adm-card img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--radius-sm); background: var(--bg-sunken); }
|
||
/* A frame whose RAW carries no preview of its own: the tile stands in for the
|
||
picture and opens the file just the same. */
|
||
.lib-noimg {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
aspect-ratio: 4 / 3;
|
||
border-radius: var(--radius-sm);
|
||
background: var(--bg-sunken);
|
||
color: var(--text-dim);
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
letter-spacing: 0.14em;
|
||
cursor: pointer;
|
||
}
|
||
.adm-meta { display: flex; flex-direction: column; gap: 2px; font-size: 11.5px; color: var(--text-dim); min-width: 0; }
|
||
.adm-meta b { color: var(--text); font-family: var(--mono); }
|
||
.adm-meta span { overflow-wrap: anywhere; }
|
||
.adm-danger { color: var(--danger); border-color: var(--border); }
|
||
.adm-danger:hover:not(:disabled) { border-color: var(--danger); }
|
||
|
||
/* The catalogue is one screen and never grows: the frame on the stage, the tree
|
||
beside it, and the strip pinned to the bottom edge. Whatever overflows scrolls
|
||
inside its own panel, so the page itself has no scrollbar to hide the strip
|
||
below the fold. Scoped to `.lib-root` — /admin shares the .adm-* blocks and
|
||
still scrolls as a page. */
|
||
.lib-root { display: flex; flex-direction: column; height: 100%; overflow: hidden; padding-bottom: 12px; }
|
||
.lib-root .adm-frame { flex: 1 1 auto; min-height: 0; }
|
||
.lib-root .adm-pane { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||
.lib-root .adm-note { flex: 0 0 auto; }
|
||
/* The column: title and the folder's own chips stay put, the split takes what
|
||
is left, the strip keeps its natural height on the bottom. */
|
||
.lib-root .adm-col { flex: 1 1 auto; min-height: 0; }
|
||
/* The column of folders is a list of names and a count — the stage is the point
|
||
of the screen, so the column takes the least it can and gives the rest away.
|
||
Its width is a variable the divider writes, and the divider itself is the middle
|
||
track, thin enough to read as the seam between the two panels. */
|
||
.lib-root .adm-col > .adm-split {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
align-items: stretch;
|
||
grid-template-columns: var(--lib-col, 118px) 9px minmax(0, 1fr);
|
||
gap: 0;
|
||
}
|
||
.lib-grip { cursor: col-resize; border-radius: 999px; touch-action: none; }
|
||
.lib-grip:hover, .lib-grip:focus-visible { background: var(--accent-soft); outline: none; }
|
||
.lib-root .adm-album { padding: 6px 8px; gap: 6px; justify-content: flex-start; }
|
||
.lib-root .adm-album-name { flex: 1 1 auto; min-width: 0; }
|
||
/* The caret column: pointing right where a row's children are folded away, down
|
||
where they are drawn, and empty on a leaf so names at one depth line up. */
|
||
.lib-caret {
|
||
flex: 0 0 auto;
|
||
width: 0;
|
||
height: 0;
|
||
border-left: 4.5px solid currentColor;
|
||
border-top: 3.5px solid transparent;
|
||
border-bottom: 3.5px solid transparent;
|
||
opacity: 0.5;
|
||
transition: transform 0.15s ease;
|
||
}
|
||
.lib-caret.open { transform: rotate(90deg); }
|
||
.lib-caret.leaf { border-left-color: transparent; }
|
||
/* A roll being read turns a ring on its own row. */
|
||
.lib-spin {
|
||
flex: 0 0 auto;
|
||
width: 11px;
|
||
height: 11px;
|
||
border-radius: 999px;
|
||
border: 2px solid var(--border);
|
||
border-top-color: var(--accent);
|
||
animation: lib-spin 0.7s linear infinite;
|
||
}
|
||
@keyframes lib-spin { to { transform: rotate(360deg); } }
|
||
/* The same ring, out in the header: the reading belongs to this tab, not to a
|
||
screen, so the studio says so too while it runs. */
|
||
.lib-scan { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.lib-spin { animation-duration: 2.4s; }
|
||
}
|
||
.lib-root .adm-col > .adm-filmstrip { flex: 0 0 auto; }
|
||
.lib-root .adm-album-list { max-height: none; flex: 1 1 auto; min-height: 0; }
|
||
.lib-root .adm-stage { min-height: 0; overflow: hidden; }
|
||
/* The frame fills the stage and the stage fits the window: `object-fit` letterboxes
|
||
it inside that box, so a 3:2 or a 4:3 frame stays whole and never pushes the
|
||
strip off screen. */
|
||
.lib-root .adm-stage-img { flex: 1 1 auto; min-height: 0; width: 100%; aspect-ratio: auto; object-fit: contain; }
|
||
.lib-root .adm-stage > .lib-noimg { flex: 1 1 auto; min-height: 0; aspect-ratio: auto; }
|
||
.lib-root .adm-stage > .adm-meta,
|
||
.lib-root .adm-stage > .chip-row { flex: 0 0 auto; }
|
||
/* All thumbs: the wall scrolls down inside the stage, and its own scrollbar on
|
||
the right edge is what reaches the frames below. */
|
||
.lib-root .adm-grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 6px; }
|
||
/* What the thumbnail wall is read through: the four selects and the count of what
|
||
is left. One row above the wall, and it does not scroll with it. */
|
||
.lib-filters { flex: 0 0 auto; align-items: center; margin-bottom: 8px; }
|
||
.lib-filters .adm-input { width: auto; min-width: 0; padding: 4px 8px; font-size: 12px; }
|
||
.lib-filters .hint { margin-left: auto; }
|
||
/* The frame's own score: five glyphs, lit up to the score it has. The star the
|
||
click lands on is the score it gives, so the row is the control. */
|
||
.lib-stars { display: flex; gap: 1px; align-items: center; }
|
||
.lib-star {
|
||
border: 1px solid transparent;
|
||
background: none;
|
||
border-radius: 6px;
|
||
padding: 2px 3px;
|
||
font-size: 15px;
|
||
line-height: 1.1;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
}
|
||
.lib-star:hover { border-color: var(--border); }
|
||
.lib-star.on { color: var(--accent); }
|
||
/* One toolbar, not two rows of chrome: ADD FOLDER, the hint, and the pair of view
|
||
icons. The hint gives up its width first and ellipsises — the whole sentence is
|
||
still there under the pointer. */
|
||
.lib-bar { flex: 0 0 auto; align-items: center; margin-bottom: 10px; }
|
||
.lib-bar .btn { padding: 5px 10px; font-size: 12.5px; }
|
||
.lib-bar-hint { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
/* The views are a glyph each (a frame, a wall of thumbs), so the row stays a row
|
||
when the window is narrow. */
|
||
.lib-icon { padding: 5px 8px; justify-content: center; }
|
||
/* A folder's own menu, on a right click. Fixed, so the tree's own scroll cannot
|
||
clip it; the position is clamped to the window when it is drawn. */
|
||
.lib-menu {
|
||
position: fixed;
|
||
z-index: 40;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 170px;
|
||
padding: 4px;
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
.lib-menu-item {
|
||
border: 0;
|
||
border-radius: var(--radius-sm);
|
||
background: none;
|
||
padding: 7px 10px;
|
||
font-size: 12px;
|
||
letter-spacing: 0.04em;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
/* Each command leads with a mark of its own — rename, scan, forget, fold — the
|
||
way the tool rail and the view switch do, so a menu read at a glance is a
|
||
column of marks rather than a block of uppercase. */
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.lib-menu-icon { flex: 0 0 auto; width: 12px; text-align: center; opacity: 0.85; }
|
||
.lib-menu-item:hover:not(:disabled) { background: var(--accent-soft); color: var(--accent); }
|
||
.lib-menu-item:disabled { opacity: 0.45; cursor: not-allowed; }
|
||
|
||
/* Settings drawer: the tabs that organise the page, plus the three panels. */
|
||
/* The management frame: a bordered box whose left column is the menu and whose
|
||
right side is the pane for the chosen section. */
|
||
.adm-frame {
|
||
display: flex;
|
||
align-items: stretch;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: var(--bg-elev);
|
||
overflow: hidden;
|
||
}
|
||
.adm-side {
|
||
flex: 0 0 200px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
padding: 12px 10px;
|
||
border-right: 1px solid var(--border);
|
||
background: var(--bg-sunken);
|
||
}
|
||
.adm-side-title {
|
||
padding: 0 8px 8px;
|
||
font-size: 11px;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--text-dim);
|
||
}
|
||
.adm-side-item {
|
||
display: block;
|
||
padding: 8px 10px;
|
||
border: 0;
|
||
border-radius: var(--radius-sm);
|
||
background: transparent;
|
||
color: var(--text);
|
||
font: inherit;
|
||
text-align: left;
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
}
|
||
.adm-side-item:hover { background: var(--bg-elev); }
|
||
.adm-side-item.on { background: var(--accent); color: var(--accent-fg); font-weight: 600; }
|
||
.adm-pane { flex: 1 1 auto; min-width: 0; padding: 14px 16px; }
|
||
/* The users table: a frame around every cell, and a head that reads as a head —
|
||
the heading face, smaller and dimmer than the rows under it. The accounts are
|
||
what the eye should land on, so the header steps back. */
|
||
.adm-table-wrap { overflow-x: auto; }
|
||
.adm-table { border-collapse: collapse; width: 100%; }
|
||
.adm-table th,
|
||
.adm-table td {
|
||
border: 1px solid var(--border);
|
||
padding: 6px 9px;
|
||
text-align: left;
|
||
vertical-align: middle;
|
||
}
|
||
.adm-table th {
|
||
background: var(--bg-sunken);
|
||
color: var(--text-dim);
|
||
font-family: var(--font-heading);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
white-space: nowrap;
|
||
}
|
||
.adm-table tbody tr:hover { background: var(--bg-sunken); }
|
||
/* The PRO box: one narrow centred column, and a box big enough to aim at. */
|
||
.adm-table th.adm-pro,
|
||
.adm-table td.adm-pro { width: 1%; text-align: center; white-space: nowrap; }
|
||
.adm-table td.adm-pro input { display: block; margin: 0 auto; cursor: pointer; }
|
||
.adm-table td.adm-pro input:disabled { cursor: default; }
|
||
/* Bulk selection: the three controls above the users table, and the checkbox
|
||
column at its head (narrow so the picture column keeps the width). */
|
||
.adm-bulk { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
|
||
.adm-bulk .btn { margin: 0; }
|
||
.adm-check { width: 1%; white-space: nowrap; }
|
||
.adm-check input { display: block; cursor: pointer; }
|
||
.adm-face {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 999px;
|
||
overflow: hidden;
|
||
background: var(--bg-sunken);
|
||
border: 1px solid var(--border);
|
||
color: var(--text-dim);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
.adm-face img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||
/* Moderation state next to the account name, and the row's own actions: the
|
||
three controls sit side by side and the pane scrolls when it runs out. */
|
||
.adm-badge-warn { margin-left: 6px; color: var(--danger); font-size: 11px; }
|
||
.adm-table td:last-child { white-space: nowrap; }
|
||
.adm-table td:last-child .btn { margin-right: 6px; }
|
||
.adm-form { display: flex; flex-direction: column; gap: 8px; max-width: 360px; }
|
||
.field-label { font-size: 11.5px; color: var(--text-dim); }
|
||
|
||
/* --- profile picture ---------------------------------------------------- */
|
||
/* One face for every control that names the signed-in user: the workspace
|
||
TopBar, the landing chip and the profile form. No picture = initial. */
|
||
.avatar {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 0 0 auto;
|
||
width: 26px;
|
||
height: 26px;
|
||
border-radius: 999px;
|
||
overflow: hidden;
|
||
background: var(--accent-soft);
|
||
border: 1px solid var(--accent-line);
|
||
color: var(--accent);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
}
|
||
.avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||
.avatar-sm { width: 20px; height: 20px; font-size: 10px; }
|
||
.avatar-row { display: flex; align-items: center; gap: 10px; }
|
||
.avatar-row .avatar { width: 46px; height: 46px; font-size: 17px; }
|
||
.account-btn { display: inline-flex; align-items: center; gap: 7px; }
|
||
/* The signed-in name rides the accent with the face beside it. */
|
||
.btn.account-btn { color: var(--accent); }
|
||
|
||
/* --- stats (admin) ------------------------------------------------------- */
|
||
/* The traffic screen: one SVG chart and a column of proportional bar lists.
|
||
No charting dependency — a couple of polylines and styled divs are enough. */
|
||
.stats-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 14px; }
|
||
.stats-bar .hint { margin: 0; }
|
||
.stats-kpis { display: flex; flex-wrap: wrap; gap: 12px; margin: 0 0 16px; }
|
||
.stats-kpi {
|
||
display: flex; flex-direction: column; gap: 2px; min-width: 104px;
|
||
padding: 10px 14px; border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm); background: var(--bg-elev);
|
||
}
|
||
.stats-kpi b { font-size: 22px; font-variant-numeric: tabular-nums; }
|
||
.stats-kpi span { font-size: 11px; letter-spacing: 0.06em; color: var(--text-dim); }
|
||
.stats-chart {
|
||
display: block; width: 100%; height: 180px;
|
||
border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-sunken);
|
||
}
|
||
.stats-area { fill: var(--accent); opacity: 0.12; }
|
||
.stats-line { fill: none; stroke: var(--accent); stroke-width: 1.5; }
|
||
.stats-line-2 { stroke: var(--text-dim); }
|
||
.stats-legend { display: flex; gap: 14px; margin: 8px 0 18px; font-size: 11px; color: var(--text-dim); }
|
||
.stats-legend i { display: inline-block; width: 10px; height: 3px; border-radius: 2px; margin-right: 5px; vertical-align: middle; }
|
||
.stats-axis { margin: 0 0 18px; }
|
||
.stats-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px 26px; }
|
||
.stats-h { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: 0.08em; color: var(--text-dim); }
|
||
.stats-list { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
||
.stats-row { display: grid; grid-template-columns: minmax(80px, 34%) 1fr auto; align-items: center; gap: 8px; font-size: 12px; }
|
||
.stats-key { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-dim); }
|
||
.stats-track { height: 8px; border-radius: 4px; background: var(--bg-sunken); overflow: hidden; }
|
||
.stats-track i { display: block; height: 100%; border-radius: 4px; background: var(--accent); }
|
||
.stats-n { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||
|
||
/* --- studio sidebar (DevelopPanels) ------------------------------------- */
|
||
/* Lightroom's right column: every panel's knobs are on screen at once, each
|
||
panel a disclosure that folds its own rows away. Colour comes from the tokens
|
||
only, so a theme or accent change lands here with no rules of its own. */
|
||
/* The sidebar (DevelopPanels) draws rows, not chips, so its column is the wide
|
||
one: the same width CREATE RECIPES takes, and the chips that are not knobs
|
||
(AUTO, TONE CURVE) stack over it, wrapped in the column's own .chip-row. */
|
||
.col-dev { width: 320px; }
|
||
.dev-panels { display: flex; flex-direction: column; gap: 10px; }
|
||
.dev-panel { border-top: 1px solid var(--border-soft); padding-top: 8px; }
|
||
.dev-panel-head {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
width: 100%; padding: 0; border: 0; background: transparent;
|
||
font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
|
||
color: var(--text); cursor: pointer;
|
||
}
|
||
.dev-panel-head:hover { color: var(--accent); }
|
||
/* The caret is the panel's whole affordance — the head is a line of uppercase
|
||
and says nothing about being a control — so it is drawn larger than the label
|
||
it sits beside, and takes the accent while the pointer is on the head. */
|
||
.dev-caret { font-size: 17px; line-height: 1; opacity: 0.75; }
|
||
.dev-panel-head:hover .dev-caret { opacity: 1; }
|
||
.dev-panel-body { display: flex; flex-direction: column; gap: 10px; padding-top: 8px; }
|
||
/* A pick that is a set of its own rather than a row of the panel's: framed and
|
||
named, so the four stops of D.RANGE read as one setting and not as four more
|
||
rows of DETAIL & EFFECTS. No colour rules: the frame is the token's. */
|
||
.dev-box { border: 1px solid var(--border-soft); border-radius: var(--radius-sm); padding: 8px; }
|
||
.dev-box-title {
|
||
display: block; margin-bottom: 6px;
|
||
font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-dim);
|
||
}
|
||
/* A row that cannot be edited at all: a PRO knob on the LITE build. It reads as
|
||
a row, not as a button — the tag is what says the row is not a slider. */
|
||
.dev-row { display: flex; flex-direction: column; gap: 4px; }
|
||
.dev-row.locked {
|
||
flex-direction: row; align-items: center; gap: 6px;
|
||
width: 100%; padding: 0; border: 0; background: transparent;
|
||
text-align: left; opacity: 0.5; cursor: not-allowed;
|
||
}
|
||
.dev-pro { font-size: 9px; letter-spacing: 0.08em; color: var(--accent); }
|
||
/* WB's two knobs: the track is the hue the value moves towards, the way
|
||
Lightroom's temperature and tint sliders are painted. Everything else keeps
|
||
the plain input's accent color. */
|
||
.track-temp, .track-tint {
|
||
height: 4px; border-radius: 999px;
|
||
appearance: none; -webkit-appearance: none;
|
||
}
|
||
.track-temp { background: linear-gradient(90deg, #3f6fd8, #b9c2cc 50%, #e8a33d); }
|
||
.track-tint { background: linear-gradient(90deg, #3f9d55, #b9c2cc 50%, #c04ec4); }
|
||
.track-temp::-webkit-slider-thumb, .track-tint::-webkit-slider-thumb {
|
||
appearance: none; -webkit-appearance: none;
|
||
width: 12px; height: 12px; border-radius: 50%;
|
||
background: var(--text); border: none; cursor: pointer;
|
||
}
|
||
|
||
/* --- responsive --------------------------------------------------------- */
|
||
@media (max-width: 860px) {
|
||
.workspace { flex-direction: column; }
|
||
/* The frame's left column becomes a wrapping row above the pane. */
|
||
.adm-frame { flex-direction: column; }
|
||
.adm-side { flex: 0 0 auto; flex-direction: row; flex-wrap: wrap; border-right: 0; border-bottom: 1px solid var(--border); }
|
||
/* The header wraps on narrow screens, so a menu-wrap can land at the start of
|
||
a row — the popover is right-anchored to its button and would hang off the
|
||
left of the screen (measured: x=-280 of 390px). Anchor it to the full-width
|
||
header instead. */
|
||
/* The bar stays at the top of the screen: the page itself never scrolls (the
|
||
stage does), but the declaration is what makes that a promise rather than a
|
||
coincidence. Sticky, not fixed — a positioned box still anchors the menus. */
|
||
.header { position: sticky; top: 0; z-index: 5; }
|
||
.menu-wrap { position: static; }
|
||
.popover { right: 8px; max-width: calc(100vw - 16px); }
|
||
/* Four labelled buttons — LIBRARY, RESET, SAVE PHOTO, EXPORT — are most of what
|
||
wraps the bar onto a third row on a 390px screen: the row's own content is
|
||
990px wide with the words and 716px with icons, and the bar measured 135px
|
||
tall before this, 88px after. They wear icons instead. Font-size 0 hides the
|
||
text from the paint, not from the DOM: the accessible name each button
|
||
already had is the word that is no longer drawn. */
|
||
.header [data-key="nav-library"],
|
||
.header [data-key="reset-look"],
|
||
.header [data-key="save-photo"],
|
||
.header [data-key="export-photo"] {
|
||
width: 34px;
|
||
height: 30px;
|
||
padding: 0;
|
||
flex: 0 0 auto;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0;
|
||
}
|
||
.header [data-key="nav-library"]::before,
|
||
.header [data-key="reset-look"]::before,
|
||
.header [data-key="save-photo"]::before,
|
||
.header [data-key="export-photo"]::before { font-size: 15px; line-height: 1; }
|
||
/* The count is the label it was hiding in, so the PRO marker goes with it —
|
||
the badge would be a 9px word in a 34px button. */
|
||
.header [data-key="save-photo"] .pro { display: none; }
|
||
.header [data-key="nav-library"]::before { content: '▤'; }
|
||
.header [data-key="reset-look"]::before { content: '⟲'; }
|
||
.header [data-key="save-photo"]::before { content: '⤓'; }
|
||
.header [data-key="export-photo"]::before { content: '↗'; }
|
||
/* --- the phone's workspace ---------------------------------------------
|
||
One strip at a time in the bottom bar: the rail of tabs, or — once a tab
|
||
is picked — that tab's own chips standing in the rail's place (the bar
|
||
never grows a second row, see .strip-open). What sits above the bar is
|
||
built out of the columns themselves, so .chips stops being a box and its
|
||
columns stand in the workspace's own stack, ordered bottom-up: the tab's
|
||
chips in the bar, the open chip's sub-chips over them, the open ruler over
|
||
those, and the photo taking all that is left. */
|
||
.chips { display: contents; }
|
||
.workspace:not(.strip-open) .chips { display: none; }
|
||
.workspace.strip-open .rail { display: none; }
|
||
.stage-slot { order: 0; min-height: 0; }
|
||
.col[data-col="slider"] { order: 1; }
|
||
.col-sub { order: 2; }
|
||
.col-main { order: 3; }
|
||
.rail { order: 4; }
|
||
|
||
/* The rail takes the shape the Android app gives it: one scrolling row of
|
||
text pills, no glyph, uppercase mono, the open tab amber. It is the bar at
|
||
the foot of the screen, so it carries the home-indicator inset itself. */
|
||
.rail {
|
||
width: auto;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 10px;
|
||
border-right: none;
|
||
border-top: 1px solid var(--border-soft);
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
|
||
scrollbar-width: none;
|
||
}
|
||
.rail::-webkit-scrollbar { display: none; }
|
||
/* The column is a row here, and the row is the tabs: a build name at its foot
|
||
would sit past the end of a line that already scrolls. It goes. */
|
||
.rail-version { display: none; }
|
||
/* A pill the thumb can actually hit: 10px/8x12 rendered ~28px tall, under half
|
||
a finger. The floor is now a 44px target and the label steps up with it. */
|
||
.rail button {
|
||
flex: 0 0 auto;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 44px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 999px;
|
||
background: var(--bg-sunken);
|
||
padding: 0 16px;
|
||
font-family: var(--mono);
|
||
font-weight: 700;
|
||
letter-spacing: 0.1em;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
}
|
||
.rail button .glyph { display: none; }
|
||
.rail button[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-line); }
|
||
|
||
.col {
|
||
width: auto;
|
||
padding: 8px;
|
||
gap: 6px;
|
||
overflow: visible;
|
||
border-right: none;
|
||
}
|
||
/* One chip per line is the desktop column's shape; a phone wants the row. */
|
||
.col .chip-row { flex-direction: row; align-items: center; }
|
||
.col .chip { text-align: center; }
|
||
.col-slider, .col-create { width: auto; }
|
||
|
||
/* --- the tab's own chips: the bottom bar -------------------------------- */
|
||
/* A strip reads across, never down: what does not fit scrolls sideways, so
|
||
the bar keeps exactly one chip's height whatever the tab holds. */
|
||
.col-main {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 6px;
|
||
max-height: 46vh;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
background: var(--bg);
|
||
border-top: 1px solid var(--border-soft);
|
||
/* No inset here any more: the path under the strip (.crumb) is the last
|
||
line of the phone's stack now, so it carries the home indicator. */
|
||
padding: 6px 8px;
|
||
}
|
||
/* The strip is as wide as its chips and the bar scrolls: a row that was
|
||
allowed to shrink would squeeze under its own contents and the chips would
|
||
paint over the ones beside them. */
|
||
.col-main > *, .col-sub:not(.wm-extra) > * { flex: 0 0 auto; }
|
||
.col-main:not(.col-create) .chip-row { flex-wrap: nowrap; }
|
||
.col-main .chip { flex: 0 0 auto; }
|
||
/* The rule a strip is read across runs the other way here: in a row of chips
|
||
it divides the line, it does not break it. */
|
||
.col-main .chip-rule { flex: 0 0 auto; width: 1px; height: 18px; margin: 0 3px; }
|
||
/* RESET is ruled off the tab's chips at the foot of a desktop column; in the
|
||
bar the rule moves to their side. The topbar's RESET already covers every
|
||
other tab (it and this row call the same reset), so the row only survives
|
||
where the button does something the topbar's does not: CREATE's form. */
|
||
.reset-row { display: none; }
|
||
.col-create .reset-row { display: block; margin: 0; padding: 0 0 0 8px; border-top: none; border-left: 1px solid var(--border); }
|
||
/* CREATE's form is not a strip of chips: it keeps the column's shape and
|
||
scrolls down instead of across, in the bar's place. */
|
||
.col-main.col-create {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
}
|
||
/* LIGHT is a strip of chips like any other tab, and its chips are its four
|
||
panels — so the widths of the panels' heads come off and the row stands in
|
||
the wrapper's place, exactly as .chips itself stands in its columns'. */
|
||
.col-dev .dev-chips { display: contents; }
|
||
.col-dev .dev-panels { display: none; }
|
||
|
||
/* --- the way back up: the path under the strip -------------------------- */
|
||
/* TABS > LIGHT > WB. The bar is as deep as the visitor walked, so the bar's
|
||
own chips cannot say it; the path can, and each name in it returns to the
|
||
strip it names. It is the last line of the stack, so it, not the strip,
|
||
carries the home indicator. */
|
||
.crumb {
|
||
order: 5;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
overflow-x: auto;
|
||
padding: 0 10px calc(6px + env(safe-area-inset-bottom));
|
||
background: var(--bg);
|
||
border-top: 1px solid var(--border-soft);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--text-dim);
|
||
}
|
||
.crumb::-webkit-scrollbar { display: none; }
|
||
.crumb-part { display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto; }
|
||
.crumb-sep { opacity: 0.5; }
|
||
/* A name in the path is a button wherever a strip answers to it, and plain
|
||
where the visitor already stands. */
|
||
.crumb-btn, .crumb-here { padding: 6px 2px; }
|
||
.crumb-btn {
|
||
border: 0;
|
||
background: none;
|
||
font: inherit;
|
||
letter-spacing: inherit;
|
||
text-transform: inherit;
|
||
color: var(--text-dim);
|
||
cursor: pointer;
|
||
}
|
||
.crumb-btn:hover { color: var(--text); }
|
||
.crumb-here { color: var(--accent); }
|
||
|
||
/* --- the strip a chip opens takes the bar -------------------------------- */
|
||
/* One strip at a time, as on the rail: a chip that opens a strip of its own
|
||
takes the bar with it, and the tab's own chips stand down. The way back to
|
||
them is the path under the bar (.crumb), which names the strip they are —
|
||
so a second row of chips over the first would only repeat where the finger
|
||
already is, and charge the photo a row for it while a knob is being moved.
|
||
Only the two columns that ARE a strip a chip opened displace the bar: the
|
||
RECIPES list, a photo's history and the mask's own column are an open
|
||
chip's content, not the strip its chip opened, and they keep the bar in
|
||
sight under them. Both wear the bar's slot and the bar's own skin, not the
|
||
sub-column's — this row IS the bar now. */
|
||
.col-sub[data-col="options"], .col-sub[data-col="wm"] {
|
||
order: 3;
|
||
background: var(--bg);
|
||
max-height: 46vh;
|
||
}
|
||
/* A watermark's column is not a row of chips: it keeps the column's shape in
|
||
the slot, with the bar's rule over it and the same ceiling. */
|
||
.col-sub[data-col="wm"] { border-top: 1px solid var(--border-soft); overflow-y: auto; }
|
||
.workspace:has(.chips > .col-sub[data-col="options"]) .col-main,
|
||
.workspace:has(.chips > .col-sub[data-col="wm"]) .col-main { display: none; }
|
||
|
||
/* One strip at a time holds here too: the ruler a knob opens is the last
|
||
strip of the walk, so it takes the place of the strip that opened it. The
|
||
panel it came from stands down rather than sitting under the ruler it
|
||
launched — the path under the bar (.crumb) already names that panel, so
|
||
the row the panel would hold is a row of the photo spent on a name the
|
||
finger can read below. With the strip and the bar both gone the ruler is
|
||
the only thing left between the photo and the path, which is the slot. */
|
||
.workspace:has(.col[data-col="slider"]) .col-sub[data-col="options"],
|
||
.workspace:has(.col[data-col="slider"]) .col-main { display: none; }
|
||
|
||
/* --- a chip's own sub-chips: the pills the strip is drawn as ------------- */
|
||
.col-sub:not(.wm-extra) {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 8px;
|
||
border-top: 1px solid var(--border-soft);
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
}
|
||
.col-sub:not(.wm-extra) .chip-row { flex-wrap: nowrap; }
|
||
/* The strip's own name is a heading, not a pick: it keeps the leading edge of
|
||
the line and drops the button around it. */
|
||
.col-sub .chip[data-key^="hint-"] {
|
||
width: auto;
|
||
height: auto;
|
||
padding: 0 4px 0 0;
|
||
border: 0;
|
||
background: none;
|
||
opacity: 1;
|
||
font-family: var(--mono);
|
||
font-size: 9px;
|
||
letter-spacing: 0.1em;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
/* --- the open ruler: the strip's own slot ------------------------------ */
|
||
/* A ruler is the photo's own scale, not a panel: no box, no fill, just the
|
||
ticks. They are drawn in the theme's own ink — dark on the light theme,
|
||
light on the dark — at two weights, a finer mark every eighth of the track
|
||
and a heavier one every fourth. */
|
||
.col[data-col="slider"] {
|
||
width: auto;
|
||
padding: 2px 8px 6px;
|
||
background: none;
|
||
border: 0;
|
||
}
|
||
.col[data-col="slider"] .slider-row {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 0;
|
||
border: 0;
|
||
background: none;
|
||
}
|
||
/* The way back is the path under the bar (.crumb), which names the panel the
|
||
ruler came from — the strip this "<" would reopen, one row lower and with
|
||
the rest of the walk beside it. A wide screen draws no path, so the button
|
||
is the phone's alone to drop. */
|
||
.col[data-col="slider"] .back { display: none; }
|
||
.col[data-col="slider"] input[type="range"] {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
/* A finger is wider than the ruler. The strip the ticks are drawn on is
|
||
22px tall, and a thumb that lands a dozen pixels off its centre is on the
|
||
photo above it or the row below — the drag never begins. The input's own
|
||
box is the target, so it grows to the finger; the track inside it keeps
|
||
the 22px the ruler is read at, because the pseudo-elements below set that
|
||
height themselves. touch-action none because the drag belongs to the
|
||
knob: nothing here may be taken for a scroll, a pull-to-refresh or a
|
||
URL-bar slide instead, and this row sits where those begin. */
|
||
height: 44px;
|
||
background: none;
|
||
touch-action: none;
|
||
}
|
||
/* One rule per engine: a grouped selector that names a pseudo-element the
|
||
engine does not know — `-moz-range-track` in Chromium — is dropped whole,
|
||
and the ticks went with it (measured: no track at all). */
|
||
.col[data-col="slider"] input[type="range"]::-webkit-slider-runnable-track {
|
||
height: 22px;
|
||
background-color: transparent;
|
||
background-image:
|
||
repeating-linear-gradient(90deg, var(--tick-strong) 0 1px, transparent 1px 32px),
|
||
repeating-linear-gradient(90deg, var(--tick) 0 1px, transparent 1px 8px);
|
||
}
|
||
.col[data-col="slider"] input[type="range"]::-moz-range-track {
|
||
height: 22px;
|
||
background-color: transparent;
|
||
background-image:
|
||
repeating-linear-gradient(90deg, var(--tick-strong) 0 1px, transparent 1px 32px),
|
||
repeating-linear-gradient(90deg, var(--tick) 0 1px, transparent 1px 8px);
|
||
}
|
||
.col[data-col="slider"] input[type="range"]::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 3px;
|
||
height: 22px;
|
||
border: 0;
|
||
border-radius: 1px;
|
||
background: var(--accent);
|
||
}
|
||
.col[data-col="slider"] input[type="range"]::-moz-range-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 3px;
|
||
height: 22px;
|
||
border: 0;
|
||
border-radius: 1px;
|
||
background: var(--accent);
|
||
}
|
||
/* The row under the photo wears its icons on a phone. Six labelled buttons wrap
|
||
to three lines (measured 141px, 17% of an 844px screen) and take the height
|
||
the picture is trying to use; the labels are what wraps, so the labels go —
|
||
out of the paint, not out of the button, which keeps the accessible name it
|
||
always had. The hint at the end of the row follows them out: it is a
|
||
sentence, and a sentence was most of the wrapping. */
|
||
.stage .toolbar { gap: 5px; }
|
||
.stage .toolbar .btn {
|
||
width: 34px;
|
||
height: 30px;
|
||
padding: 0;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0;
|
||
}
|
||
.stage .toolbar .btn::before { font-size: 14px; line-height: 1; }
|
||
.stage .toolbar .btn .pro { display: none; }
|
||
.stage .toolbar .hint:not(.dims) { display: none; }
|
||
.stage .toolbar [data-key="clear-photo"]::before { content: '✕'; }
|
||
.stage .toolbar [data-key="open-photo"]::before { content: '↻'; }
|
||
.stage .toolbar [data-key="open-camera"]::before { content: '◉'; }
|
||
.stage .toolbar [data-key="save-original"]::before { content: '⤓'; }
|
||
.stage .toolbar [data-key="compare-toggle"]::before { content: '◐'; }
|
||
.stage .toolbar [data-key="hist-toggle"]::before { content: '▥'; }
|
||
/* The crop frame's four corner nodes. The square stays 16px — that is the size
|
||
a corner reads at — but a finger is ~44, so the touch target grows inward
|
||
from the node's own centre: the half that would grow outward is the photo's
|
||
own edge, which the layer clips anyway (measured before this: a node was
|
||
9x16px to a touch, and a thumb a few pixels off it landed on the rect and
|
||
MOVED the frame instead of resizing it). The drag maths reads the pointer
|
||
against the photo, not against the node, so a larger target does not shift
|
||
the corner. */
|
||
.crop-handle::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
width: 44px;
|
||
height: 44px;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
}
|