Files
RecipesCam/docker/frontend/src/styles/app.css
T
3dtours 8259bd468d web: move the section jumps to their own strip and grow the Theme menu
The six section links leave the top bar for a thin shelf under it, each a
bordered pill at the page's normal text size, so the bar itself stays a
row of actions and the jumps still read as buttons.

The Theme menu gains the workspace's colour groups — the landing palette
now takes its hue from the accent tokens — an Auto mode that follows the
OS scheme live, and a dropdown in place of the three font chips.
2026-09-18 11:30:12 +07:00

493 lines
17 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(--text-dim); }
/* 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; }
.recipe-name {
font-family: var(--mono);
font-size: 12px;
color: var(--text-dim);
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); }
/* --- 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); }
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
/* 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; }
.wm-extra { border-right-color: var(--accent-line); }
.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); }
.chip[aria-pressed="true"], .chip.on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
/* 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 .pro { font-size: 9px; letter-spacing: 0.08em; color: var(--accent); }
.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; }
.slider-row .head { display: flex; justify-content: space-between; font-size: 11px; letter-spacing: 0.1em; color: var(--text-dim); }
.slider-row .head b { 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; }
/* --- 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; }
.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; }
.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; }
.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; }
.err { color: var(--danger); font-size: 12px; max-width: 520px; text-align: center; }
/* --- 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; }
/* --- 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(--text-dim); }
.adm-brand img { border-radius: 8px; }
.adm-slot { display: flex; flex-direction: column; gap: 3px; letter-spacing: 0.04em; }
.adm-slot select { width: 100%; }
.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); }
.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); }
/* 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; }
.adm-table-wrap { overflow-x: auto; }
/* 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; }
.adm-filter { display: flex; align-items: center; gap: 8px; }
/* 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(--bg-sunken);
border: 1px solid var(--border);
color: var(--text-dim);
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; }
/* --- 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. */
.header { position: relative; }
.menu-wrap { position: static; }
.popover { right: 8px; max-width: calc(100vw - 16px); }
.rail {
order: 2;
width: auto;
flex-direction: row;
border-right: none;
border-top: 1px solid var(--border-soft);
overflow-x: auto;
padding: 4px;
}
.rail button { flex: 1 0 auto; min-width: 62px; }
/* The cascade still runs left to right; the columns themselves scroll rather
than the page, so the stage keeps its width. */
.chips {
order: 1;
width: auto;
max-width: none;
max-height: 42vh;
border-right: none;
border-bottom: 1px solid var(--border-soft);
}
.col { width: 148px; padding: 8px; }
.col-slider { width: 200px; }
.col-create { width: 260px; }
.stage { order: 0; flex: 1 1 auto; }
}